Editing Talk:SC Communication

Jump to navigation Jump to search
Warning: You are not logged in. Your IP address will be publicly visible if you make any edits. If you log in or create an account, your edits will be attributed to your username, along with other benefits.

The edit can be undone. Please check the comparison below to verify that this is what you want to do, and then publish the changes below to finish undoing the edit.

Latest revision Your text
Line 1: Line 1:
== The easy peasy lemon squeasy way (UART) (CXR/CXRF/SW) ==
{{boxcode|height=400px|title=SysconAUTH.py|code=<syntaxhighlight lang=python>
# Python 2 + 3 compatible
from binascii import unhexlify as uhx
from Crypto.Cipher import AES # pycryptodome
import os
import serial # pyserial
import string
import sys
import time
class PS3UART(object):
    ser = serial.Serial()
    type = ''
           
    sc2tb = uhx('71f03f184c01c5ebc3f6a22a42ba9525')  # Syscon to TestBench Key    (0x130 xor 0x4578)
    tb2sc = uhx('907e730f4d4e0a0b7b75f030eb1d9d36')  # TestBench to Syscon Key    (0x130 xor 0x4588)
    value = uhx('3350BD7820345C29056A223BA220B323')  # 0x45B8
    zero  = uhx('00000000000000000000000000000000')
    auth1r_header = uhx('10100000FFFFFFFF0000000000000000')
    auth2_header  = uhx('10010000000000000000000000000000')
   
    def aes_decrypt_cbc(self, key, iv, in_data):
        return AES.new(key, AES.MODE_CBC, iv).decrypt(in_data)
   
    def aes_encrypt_cbc(self, key, iv, in_data):
        return AES.new(key, AES.MODE_CBC, iv).encrypt(in_data)
    def __init__(self, port, type):
        self.ser.port = port
        if(type == 'CXR' or type == 'SW'):
            self.ser.baudrate = 57600
        elif(type == 'CXRF'):
            self.ser.baudrate = 115200
        else:
            assert(False)
        self.type = type
        self.ser.timeout = 0.1
        self.ser.open()
        assert(self.ser.isOpen())
        self.ser.flush()
       
    def __del__(self):
        self.ser.close()
       
    def send(self, data):
        self.ser.write(data.encode('ascii'))   
                           
    def receive(self):
        return self.ser.read(self.ser.inWaiting())
       
    def command(self, com, wait = 1, verbose = False):
        if(verbose):
            print('Command: ' + com)
       
        if(self.type == 'CXR'):       
            length = len(com)
            checksum = sum(bytearray(com, 'ascii')) % 0x100
            if(length <= 10):
                self.send('C:{:02X}:{}\r\n'.format(checksum, com))
            else:
                j = 10
                self.send('C:{:02X}:{}'.format(checksum, com[0:j]))
                for i in range(length - j, 15, -15):
                    self.send(com[j:j+15])
                    j += 15
                self.send(com[j:] + '\r\n')
        elif(self.type == 'SW'):   
            length = len(com)
            if(length >= 0x40):
                if(self.command('SETCMDLONG FF FF')[0] != 0):
                    return (0xFFFFFFFF, ['Setcmdlong'])       
            checksum = sum(bytearray(com, 'ascii')) % 0x100
            self.send('{}:{:02X}\r\n'.format(com, checksum))
        else:
            self.send(com + '\r\n')
           
        time.sleep(wait)
        answer = self.receive().decode('ascii').strip()
        if(verbose):
            print('Answer: ' + answer)
       
        if(self.type == 'CXR'):
            answer = answer.split(':')
            if(len(answer) != 3):
                return (0xFFFFFFFF, ['Answer length'])
            checksum = sum(bytearray(answer[2], 'ascii')) % 0x100
            if(answer[0] != 'R' and answer[0] != 'E'):
                return (0xFFFFFFFF, ['Magic'])
            if(answer[1] != '{:02X}'.format(checksum)):
                return (0xFFFFFFFF, ['Checksum'])   
            data = answer[2].split(' ')
            if(answer[0] == 'R' and len(data) < 2 or answer[0] == 'E' and len(data) != 2):
                return (0xFFFFFFFF, ['Data length'])
            if(data[0] != 'OK' or len(data) < 2):
                return (int(data[1], 16), [])
            else:
                return (int(data[1], 16), data[2:])   
        elif(self.type == 'SW'):
            answer = answer.split('\n')
            for i in range(0, len(answer)):
                answer[i] = answer[i].replace('\n', '').rsplit(':', 1)
                if(len(answer[i]) != 2):
                    return (0xFFFFFFFF, ['Answer length'])
                checksum = sum(bytearray(answer[i][0], 'ascii')) % 0x100
                if(answer[i][1] != '{:02X}'.format(checksum)):
                    return (0xFFFFFFFF, ['Checksum'])
                answer[i][0] += '\n'
            ret = answer[-1][0].replace('\n', '').split(' ')
            if(len(ret) < 2 or len(ret[1]) != 8 and not all(c in string.hexdigits for c in ret[1])):
                return (0, [x[0] for x in answer])
            elif(len(answer) == 1):       
                return (int(ret[1], 16), ret[2:])
            else:
                return (int(ret[1], 16), [x[0] for x in answer[:-1]])
        else:
            return (0, [answer])
           
    def auth(self):
        if(self.type == 'CXR' or self.type == 'SW'):
            auth1r = self.command('AUTH1 10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')
            if(auth1r[0] == 0 and auth1r[1] != []):
                auth1r = uhx(auth1r[1][0])
                if(auth1r[0:0x10] == self.auth1r_header):
                    data = self.aes_decrypt_cbc(self.sc2tb, self.zero, auth1r[0x10:0x40])
                    if(data[0x8:0x10] == self.zero[0x0:0x8] and data[0x10:0x20] == self.value and data[0x20:0x30] == self.zero):
                        new_data = data[0x8:0x10] + data[0x0:0x8] + self.zero + self.zero
                        auth2_body = self.aes_encrypt_cbc(self.tb2sc, self.zero, new_data)
                        auth2r = self.command('AUTH2 ' + ''.join('{:02X}'.format(c) for c in bytearray(self.auth2_header + auth2_body)))
                        if(auth2r[0] == 0):
                            return 'Auth successful'
                        else:
                            return 'Auth failed'
                    else:
                        return 'Auth1 response body invalid'
                else:
                    return 'Auth1 response header invalid'
            else:
                return 'Auth1 response invalid'
        else:
            scopen = self.command('scopen')
            if('SC_READY' in scopen[1][0]):
                auth1r = self.command('10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')
                auth1r = auth1r[1][0].split('\r')[1][1:]
                if(len(auth1r) == 128):
                    auth1r = uhx(auth1r)
                    if(auth1r[0:0x10] == self.auth1r_header):
                        data = self.aes_decrypt_cbc(self.sc2tb, self.zero, auth1r[0x10:0x40])
                        if(data[0x8:0x10] == self.zero[0x0:0x8] and data[0x10:0x20] == self.value and data[0x20:0x30] == self.zero):
                            new_data = data[0x8:0x10] + data[0x0:0x8] + self.zero + self.zero
                            auth2_body = self.aes_encrypt_cbc(self.tb2sc, self.zero, new_data)
                            auth2r = self.command(''.join('{:02X}'.format(c) for c in bytearray(self.auth2_header + auth2_body)))
                            if('SC_SUCCESS' in auth2r[1][0]):
                                return 'Auth successful'
                            else:
                                return 'Auth failed'
                        else:
                            return 'Auth1 response body invalid'
                    else:
                        return 'Auth1 response header invalid'
                else:
                    return 'Auth1 response invalid'
            else:
                return 'scopen response invalid'
   
def main(argc, argv):
    if(argc < 3):
        print(os.path.basename(__file__) + ' <serial port> <sc type ["CXR", "CXRF", "SW"]>')
        sys.exit(1)
    ps3 = PS3UART(argv[1], argv[2])
    raw_input_c = vars(__builtins__).get('raw_input', input)
    while True:
        in_data = raw_input_c('> ')
        if(in_data.lower() == 'auth'):
            print(ps3.auth())
            continue
        if(in_data.lower() == 'exit'):
            break
        ret = ps3.command(in_data)
        if(argv[2] == 'CXR'):
            print('{:08X}'.format(ret[0]) + ' ' +  ' '.join(ret[1]))
        elif(argv[2] == 'SW'):
            if(len(ret[1]) > 0 and '\n' not in ret[1][0]):
                print('{:08X}'.format(ret[0]) + ' ' + ' '.join(ret[1]))
            else:
                print('{:08X}'.format(ret[0]) + '\n' + ''.join(ret[1]))
        else:
            print(ret[1][0])
               
if __name__ == '__main__':
    main(len(sys.argv), sys.argv)
</syntaxhighlight>}}
* Credits to M4j0r for the RE of this important info
=== Example Scripts ===
{{boxcode|height=400px|title=SysconEEPdumpCXR.py|code=<syntaxhighlight lang=python>
# Python 2 compatible
if(len(sys.argv) < 3):
    print os.path.basename(__file__) + ' <serial port> <output file>'
    sys.exit(1)
ps3 = PS3UART(sys.argv[1], 'CXR')
print "Version: " + ps3.command("VER")[1][0]
print ps3.auth()
f = open(sys.argv[2], 'wb')
block_size = 0x40
print "Dumping NVS"
failed = []
for i in xrange(0x2C00, 0x7400, block_size): # 0x7400 for CXR713, 0x4400 for CXR714
    print "Reading 0x{:04X}".format(i)
    data = ps3.command("R8 {:08X} {:02X}".format(i, block_size))
    ret = data[0]
    if ret == 0:
        f.write((data[1][0]).decode('hex'))
    else:
        print "Failed: " + str(ret)
        failed += [i]
        f.write(("A"*block_size*2).decode('hex'))
   
f.close()
time.sleep(2)
print "\nRetrying failed offsets"
for i in failed:
    print "Reading 0x{:04X}".format(i)
    for j in xrange(0, block_size, block_size/4):
        while True:
            data = ps3.command("R8 {:08X} {:02X}".format(i+j, block_size/4))
            ret = data[0]
            if ret == 0:
                print data[1][0]
                break
            time.sleep(2)
</syntaxhighlight>}}
{{boxcode|height=400px|title=SysconEEPdumpCXRF.py|code=<syntaxhighlight lang=python>
# Python 2 + 3 compatible
if(len(sys.argv) < 3):
    print(os.path.basename(__file__) + ' <serial port> <output file>')
    sys.exit(1)
 
ps3 = PS3UART(sys.argv[1], 'CXRF')
print('Version: ' + ps3.command('version')[1][0].split('\n')[1])
print(ps3.auth())
f = open(sys.argv[2], 'wb')
block_size = 0x40
print('Dumping NVS')
for i in range(0x2C00, 0x7400, block_size):
    print('Reading 0x{:04X}'.format(i))
    data = ps3.command('r {:08X} {:02X}'.format(i, block_size))
    data = data[1][0].split('\n')
    if len(data) != 9:
        print('Failed')
        continue
    for i in range(3, 7):
        temp = data[i][0:-2].replace(' ', '')
        f.write(bytearray.fromhex(temp))
     
f.close()
</syntaxhighlight>}}
{{boxcode|height=400px|title=SysconEEPdumpSW.py|code=<syntaxhighlight lang=python>
# Python 2 + 3 compatible
if(len(sys.argv) < 3):
    print(os.path.basename(__file__) + ' <serial port> <output file>')
    sys.exit(1)
 
ps3 = PS3UART(sys.argv[1], 'SW')
print('Version: ' + ps3.command('VER')[1][0])
print(ps3.auth())
f = open(sys.argv[2], 'wb')
block_size = 0x40
print('Dumping NVS')
for i in range(0x0, 0x1400, block_size):
    print('Reading 0x{:04X}'.format(i))
    data = ps3.command('EEP GET {:08X} {:02X}'.format(i, block_size))
    ret = data[0]
    temp = ''
    if ret == 0:
        for i in range(2, len(data[1])):
            temp += data[1][i][2:-2].replace(' ', '')
        f.write(bytearray.fromhex(temp))
    else:
        print('Failed: ' + str(ret))
     
f.close()
</syntaxhighlight>}}
{{boxcode|height=400px|title=SysconEEPpatchCXR.py|code=<syntaxhighlight lang=python>
# Python 2 compatible
if(len(sys.argv) < 3):
    print os.path.basename(__file__) + ' <serial port> <patch file>'
    sys.exit(1)
ps3 = PS3UART(sys.argv[1], 'CXR')
f = open(sys.argv[2], 'rb')
patch = f.read()
f.close()
print "Version: " + ps3.command("VER")[1][0]
print ps3.auth()
patch_area_1 = 0x2800
patch_area_2 = 0x4400 # 0x7400 for CXR713, 0x4400 for CXR714
block_size = 0x40
print "First region"
for i in xrange(0, 0x400, block_size):
    print hex(ps3.command("EEP SET " + hex(i+patch_area_1)[2:] + " " + hex(block_size)[2:] + " " + patch[i:i+block_size].encode('hex'))[0])
print ""
print "Second region"
for i in xrange(0x400, 0x1000, block_size):
    print hex(ps3.command("EEP SET " + hex(i+patch_area_2-0x400)[2:] + " " + hex(block_size)[2:] + " " + patch[i:i+block_size].encode('hex'))[0])
</syntaxhighlight>}}
== IDs ==
<pre>
//phase ids
#define PHASE_ID_GET_STATE 0x0
#define PHASE_ID_PERSONALIZE 0x1
#define PHASE_ID_AUTH1 0x2
#define PHASE_ID_AUTH2 0x3
#define PHASE_ID_SEQ_SERVICE 0x4
#define PHASE_ID_GET_PERSONALIZE_VERSION 0x5
#define PHASE_ID_GET_SYSTEM_INFO 0x6
#define PHASE_ID_FACTORY_INIT 0xFF
//seq service ids
#define SEQ_SERVICE_ID_GET_RTC 0x1
#define SEQ_SERVICE_ID_SET_CLOCK 0x2
#define SEQ_SERVICE_ID_GET_CLOCK 0x3
#define SEQ_SERVICE_ID_READ_DATA 0x4
#define SEQ_SERVICE_ID_WRITE_DATA 0x5
#define SEQ_SERVICE_ID_GET_RTC_STATUS 0x6
#define SEQ_SERVICE_ID_SET_RTC_STATUS 0x7
#define SEQ_SERVICE_ID_SET_RTC 0x8
#define SEQ_SERVICE_ID_CORRECT_RTC 0x9
#define SEQ_SERVICE_ID_SET_RTC2 0xA
#define SEQ_SERVICE_ID_CORRECT_RTC2 0xB
</pre>
== Syscon packets ==
== Syscon packets ==


=== Device Access Service (0x03) ===
=== Timezone/Thermal Service (0x11) ===


==== EEPROM write ====
==== Get temperature ====
Purpose: Write block of data to EEPROM.
Purpose: Used to get current temperature.
<pre>
<pre>
struct __attribute__ ((packed)) dev_access_request_t {
struct __attribute__ ((packed)) get_temperature_request_t {
uint8_t cmd; // 0x01
uint8_t padding[3];
uint32_t offset;
uint32_t size;
uint8_t data[0];
};
 
struct __attribute__ ((packed)) dev_access_response_t {
uint8_t status; // 0x00:OK, 0x03:Wrong offset, 0x04:Wrong size, 0xFF:Error
uint8_t data[0];
};
</pre>
 
* Although this service is not used on Slim and some Phat consoles anymore, it is working (only on consoles with a CXR (Mullion) Syscon).
* It seems it have the same restrictions as NVS service.
 
==== EEPROM read ====
Purpose: Read block of data from EEPROM.
<pre>
struct __attribute__ ((packed)) dev_access_request_t {
uint8_t cmd; // 0x00
uint8_t padding[3];
uint32_t offset;
uint32_t size;
};
 
struct __attribute__ ((packed)) dev_access_response_t {
uint8_t status; // 0x00:OK, 0x03:Wrong offset, 0x04:Wrong size, 0xFF:Error
uint8_t data[0];
};
</pre>
 
* Although this service is not used on Slim and some Phat consoles anymore, it is working (only on consoles with a CXR (Mullion) Syscon).
* It seems it have the same restrictions as NVS service.
 
=== Thermal Service (0x11) ===
 
==== Get temperature alert? ====
Purpose: Used to get current temperature alert?.
<pre>
struct __attribute__ ((packed)) get_thermal_alert_request_t {
uint8_t cmd; // 0x20
uint8_t cmd; // 0x20
uint8_t param; // 0xFF
uint8_t param; // 0xFF
};
};


struct __attribute__ ((packed)) get_thermal_alert_response_t {
struct __attribute__ ((packed)) get_temperature_response_t {
uint8_t unk1; // status?
uint8_t unk1; // status?
uint8_t unk2; // status?
uint8_t unk2; // status?
Line 407: Line 18:
</pre>
</pre>


==== Get temperature ====
* To calculate temperature in celsius use: (data[0] * 0x64) >> 8.
Purpose: Used to get current temperature.
<pre>
struct __attribute__ ((packed)) get_temperature_request_t {
uint8_t cmd; // 0x00
uint8_t tzone; // 0x00:CELL, 0x01:RSX
};
 
struct __attribute__ ((packed)) get_temperature_response_t {
uint8_t status;
int8_t temperature_hi;
int8_t temperature_lo;
};
</pre>
 
* Temperature in celsius: sprintf("%d.%d", temperature_hi, (temperature_lo * 100) / 256);


=== Configuration Service (0x12) ===
=== Configuration Service (0x12) ===
Line 440: Line 36:
};
};
</pre>
</pre>
<pre>
struct __attribute__ ((packed)) get_xdr_config2_request_t {
uint8_t cmd; // 0x01
uint8_t param; // 0x00
};
struct __attribute__ ((packed)) get_xdr_config2_response_t {
uint8_t cmd;
uint8_t data_size?;
uint8_t padding[2];
uint8_t data[256];
};
</pre>
* get_xdr_config2 returns more data (maybe different config?)


==== Get IDlog information ====
==== Get IDlog information ====
Line 473: Line 53:
</pre>
</pre>


==== sc_config_info::be::get_reference_clock ====
==== Get reference clock ====
Purpose: Used to calculate an initial value of timebase register.
Purpose: Used to calculate an initial value of timebase register.
<pre>
<pre>
struct __attribute__ ((packed)) get_reference_clock_request_t {
struct __attribute__ ((packed)) get_reference_clock_request_t {
uint8_t cmd; // 0x03
uint8_t cmd; // 0x03
uint8_t param; // 0x10
};
struct __attribute__ ((packed)) get_reference_clock_response_t {
uint8_t cmd;
uint8_t padding[3];
uint32_t ref_clock_value;
uint32_t unk;
};
</pre>
==== sc_config_info::xdr::get_reference_clock ====
<pre>
struct __attribute__ ((packed)) get_reference_clock_request_t {
uint8_t cmd; // 0x00
uint8_t param; // 0x10
uint8_t param; // 0x10
};
};
Line 581: Line 146:
struct __attribute__ ((packed)) reboot_request_t {
struct __attribute__ ((packed)) reboot_request_t {
uint8_t cmd; // 0x01
uint8_t cmd; // 0x01
};
</pre>
==== Query system power up cause ====
Purpose: Get information about system power up cause.
<pre>
struct __attribute__ ((packed)) query_system_power_up_cause_request_t {
uint8_t cmd; // 0x10
};
struct __attribute__ ((packed)) query_system_power_up_cause_response_t {
uint8_t cmd;
uint8_t padding1[3];
uint32_t wake_source;
uint8_t requested_os_context;
uint8_t current_os_context;
uint8_t requested_gr_context;
uint8_t current_gr_context;
uint8_t last_shutdown_cause;
uint8_t padding2[3];
};
</pre>
==== Get realtime clock ====
Purpose: Get current value of realtime clock.
<pre>
struct __attribute__ ((packed)) get_rtc_request_t {
uint8_t cmd; // 0x33
};
struct __attribute__ ((packed)) get_rtc_response_t {
uint8_t cmd;
uint8_t padding[3];
uint32_t rtc;
};
};
</pre>
</pre>
Line 624: Line 155:
<pre>
<pre>
struct __attribute__ ((packed)) nvs_access_request_t {
struct __attribute__ ((packed)) nvs_access_request_t {
uint8_t cmd; // 0x10
uint8_t cmd; // 0x20
uint8_t index;
uint8_t index;
uint8_t offset;
uint8_t offset;
uint8_t size; // 0x00 for full block
uint8_t size; // 0x00 for full block
uint8_t data[0];
};
};


Line 644: Line 174:
<pre>
<pre>
struct __attribute__ ((packed)) nvs_access_request_t {
struct __attribute__ ((packed)) nvs_access_request_t {
uint8_t cmd; // 0x20
uint8_t cmd; // 0x10
uint8_t index;
uint8_t index;
uint8_t offset;
uint8_t offset;
Line 659: Line 189:
</pre>
</pre>


=== Version Service (0x18) ===
==== Query system power up cause ====
Purpose: Get information about system power up cause.
<pre>
struct __attribute__ ((packed)) query_system_power_up_cause_request_t {
uint8_t cmd; // 0x10
};
 
struct __attribute__ ((packed)) query_system_power_up_cause_response_t {
uint8_t cmd;
uint8_t padding1[3];
uint32_t wake_source;
uint8_t requested_os_context;
uint8_t current_os_context;
uint8_t requested_gr_context;
uint8_t current_gr_context;
uint8_t last_shutdown_cause;
uint8_t padding2[3];
};
</pre>
 
==== Get realtime clock ====
Purpose: Get current value of realtime clock.
<pre>
struct __attribute__ ((packed)) get_rtc_request_t {
uint8_t cmd; // 0x33
};
 
struct __attribute__ ((packed)) get_rtc_response_t {
uint8_t cmd;
uint8_t padding[3];
uint32_t rtc;
};
</pre>
 
=== Livelock Service (0x18) ===


==== Get service version ====
==== Get service version ====
Line 678: Line 242:


==== Get syscon version ====
==== Get syscon version ====
Purpose: Get information about syscon version.<br>
Purpose: Get information about syscon version.
Note: Syscon Auth required.
<pre>
<pre>
struct __attribute__ ((packed)) get_sc_version_request_t {
struct __attribute__ ((packed)) get_sc_version_request_t {
Line 689: Line 252:
uint8_t padding[3];
uint8_t padding[3];
uint16_t version;
uint16_t version;
};
</pre>
=== Livelock Service (0x1B) ===
==== Set livelock detection mode ====
<pre>
struct __attribute__ ((packed)) set_livelock_detection_mode_request_t {
uint8_t cmd; // 0x10
uint8_t param; //0x00
};
struct __attribute__ ((packed)) set_livelock_detection_mode_response_t {
uint8_t data; //
};
};
</pre>
</pre>
Line 781: Line 329:
};
};
</pre>
</pre>
==Tools==
*SysCommParser by TizzyT http://tizzyt-archive.blogspot.com.es/2015/02/syscommparser.html
==Log Info==
Each of the times a request is made that involves Auth1+Auth2,there is a communication made to eeprom for read/write. It goes like this (case for backup_srh):
* Read initial 16 bytes of eeprom once (packet 0) (0x10)
* Read 32 bytes following  20 times (packets 1-21) (0x280)
* Auth1 established
* Repeat steps 1 2
* Auth2 established
* Repeat steps 1 2 for finding key?/validation?
* Validation/Finding Key established
* Final Packet(s): Read EEPROM Location/Write to EEPROM Location/Do Other Tasks
Notes:
* Initial bytes are constant and never change from phat to phat
* Following bytes are perconsole (EID1 most likely)
* Steps that don't involve sc auth1 auth2 such as reading eeprom using nvs service or checking syscon status (probably stored in RAM, because true value is in EEPROM encrypted) have no packet activity
* initial bytes value: 99 D9 66 2B B3 D7 61 54 6B 9C 3F 9E D1 40 ED B0
* we haven't tested bootldr init yet, but we think it might be different from the others...
Credits to [[User:Zer0Tolerance|ZeroTolerance]] for the info
=== Log ===
http://pastie.org/private/0pnjuk1hp3qybqdjtpryq
* KickStart Value (packet 0): 1.8480055 seconds
* KickEnd Value : (packet 69): 5.8694735 seconds
* Diff : 4.021468 seconds
= LiveLock and Versioning Confusion =
See :
* https://www.psdevwiki.com/ps3/File:C1kVeMD.png
* https://www.psdevwiki.com/ps3/File:NBkrqqZ.png
* database : https://www.sendspace.com/file/6awfi9
For proof
Please note that all contributions to PS3 Developer wiki are considered to be released under the GNU Free Documentation License 1.2 (see PS3 Developer wiki:Copyrights for details). If you do not want your writing to be edited mercilessly and redistributed at will, then do not submit it here.
You are also promising us that you wrote this yourself, or copied it from a public domain or similar free resource. Do not submit copyrighted work without permission!

To protect the wiki against automated edit spam, we kindly ask you to solve the following hCaptcha:

Cancel Editing help (opens in new window)

Template used on this page: