databankr

Casio Databank/Telememo record format encoder/decoder
git clone git://git.luxferre.top/databankr.git
Log | Files | Refs | README

databankr.py (7267B)


      1 #!/usr/bin/env python3
      2 # Databankr: a CLI tool to encode arbitrary data to Casio Databank/Telememo 
      3 # watches and restore it from there
      4 # Created by Luxferre in 2024, released into public domain
      5 
      6 import sys, math, json, re
      7 
      8 # universal base conversion methods
      9 
     10 def to_base(number, base, charset):
     11   if not number:
     12     return charset[0]
     13   res = ''
     14   while number > 0:
     15     res = charset[number % base] + res
     16     number //= base
     17   return res
     18 
     19 def from_base(number, base, charset):
     20   if number == charset[0]:
     21     return 0
     22   res = 0
     23   for c in number:
     24     ind = charset.find(c)
     25     if ind > -1:
     26       res = res * base + ind
     27     else:
     28       return 0
     29   return res
     30 
     31 # record preparation methods
     32 
     33 # create a padded field from a numeric value
     34 def val_to_field(n, charset, padlen):
     35   return to_base(n, len(charset), charset).rjust(padlen, charset[0])
     36 
     37 # main encoding method
     38 def encode(data: bytes, config):
     39   effective_namelen = config['namelen'] - 1
     40   alphabase = len(config['alpha'])
     41   numbase = len(config['digit'])
     42   indexsize = len(config['index'])
     43   # calculate record estimation
     44   name_part_bits = int(effective_namelen * math.log2(alphabase))
     45   number_part_bits = int(config['numberlen'] * math.log2(numbase))
     46   record_bits = name_part_bits + number_part_bits # single record capacity
     47   max_bits = record_bits * indexsize # overall databank capacity
     48   # start processing
     49   bitstr = ''.join(f'{c:08b}' for c in data) # create an aligned bitstring
     50   bitlen = len(bitstr) # overall bitstring length
     51   if bitlen > max_bits: # truncate the excess
     52     bitlen = max_bits
     53     bitstr = bitstr[:max_bits]
     54   rec_len = int(math.ceil(bitlen / record_bits)) # message length in records
     55   records = [] # list of lists
     56   pos = 0 # current position tracker
     57   for i in range(0, rec_len): # slice over the bitstring
     58     namebin = bitstr[pos:pos+name_part_bits].ljust(name_part_bits, '0')
     59     pos += name_part_bits
     60     numbin = bitstr[pos:pos+number_part_bits].ljust(number_part_bits, '0')
     61     pos += number_part_bits
     62     # now we only got binary representation of both parts
     63     # let's convert them to bigints and then to the actual records
     64     namefield = config['index'][i] + val_to_field(int(namebin, 2), 
     65                   config['alpha'], effective_namelen)
     66     numfield = val_to_field(int(numbin, 2), 
     67                   config['digit'], config['numberlen'])
     68     records.append([namefield, numfield])
     69   return records
     70 
     71 # main decoding method
     72 def decode(records, config, expected=0):
     73   alphabase = len(config['alpha'])
     74   numbase = len(config['digit'])
     75   effective_namelen = config['namelen'] - 1
     76   name_part_bits = int(effective_namelen * math.log2(alphabase))
     77   number_part_bits = int(config['numberlen'] * math.log2(numbase))
     78   bitstr = '' # bit string storage
     79   for rec in records: # iterate over records
     80     nameval = from_base(rec[0][1:], alphabase, config['alpha'])
     81     numval = from_base(rec[1], numbase, config['digit'])
     82     bitstr += format(nameval, '0b').zfill(name_part_bits)
     83     bitstr += format(numval, '0b').zfill(number_part_bits)
     84   # reconstruct the raw data from the bitstring and return it
     85   datalen = int(math.ceil(len(bitstr)/8)) # estimate the data length in bytes
     86   if expected > 0: # truncate if expected length is specified
     87     datalen = expected
     88   data = b'' # raw data placeholder
     89   for i in range(0, datalen): # iterate over byte slices
     90     ind = i << 3
     91     data += int(bitstr[ind:ind+8].ljust(8, '0'), 2).to_bytes(1, 'big')
     92   return data
     93 
     94 def auto_int(x): # helps to convert from any base natively supported in Python
     95     return int(x,0)
     96 
     97 if __name__ == '__main__': # main app start
     98   from argparse import ArgumentParser
     99   parser = ArgumentParser(description='Databankr: Casio Databank/Telememo record format encoder/decoder', epilog='(c) Luxferre 2024 --- No rights reserved <https://unlicense.org>')
    100   parser.add_argument('mode', help='Operation mode (enc/dec)')
    101   parser.add_argument('-t', '--type', type=str, default='bin', help='Data type (bin/hex, default bin)')
    102   parser.add_argument('-i', '--input-file', type=str, default='-', help='Source input file (default "-", stdin)')
    103   parser.add_argument('-o', '--output-file', type=str, default='-', help='Result output file (default "-", stdout)')
    104   parser.add_argument('-c', '--config', type=str, default='config.json', help='Configuration JSON file path (default config.json in current working directory)')
    105   parser.add_argument('-m', '--module', type=str, default='2515-lat', help='Module configuration code according to your watch (default 2515-lat)')
    106   parser.add_argument('-l', '--expected-length', type=auto_int, default=0, help='Expected decoded data length in bytes (default 0 - no limits)')
    107   args = parser.parse_args()
    108 
    109   # detect the mode
    110   if args.mode == 'enc':
    111     flow = 'enc'
    112   elif args.mode == 'dec':
    113     flow = 'dec'
    114   else:
    115     print('Invalid mode! Please specify enc or dec!')
    116     exit(1)
    117 
    118   # load the configuration file
    119   try:
    120     f = open(args.config)
    121     confdata = json.load(f)
    122     f.close()
    123   except:
    124     print('Config file missing or invalid!')
    125     exit(1)
    126 
    127   # load the module config
    128   if args.module in confdata:
    129     moduleconfig = confdata[args.module]
    130     print('Loaded the configuration for %s' % moduleconfig['description'])
    131   else:
    132     print('Module configuration %s not found in the config file!' % args.module)
    133     exit(1)
    134 
    135   # load the input data
    136   try:
    137     if args.input_file == '-':
    138       infd = sys.stdin
    139     else:
    140       infd = open(args.input_file, mode='rb')
    141     indata = infd.read()
    142     if infd == sys.stdin: # perform additional conversion
    143       indata = indata.encode('utf-8')
    144     else:
    145       infd.close()
    146   except:
    147     print('Error reading the input data!')
    148     print(sys.exc_info())
    149     exit(1)
    150 
    151   # run the selected flow
    152   if flow == 'enc': # encoding flow
    153     if args.type == 'hex': # convert the input data if the type is hex
    154       indata = bytes.fromhex(re.sub(r"[^0-9a-fA-F]", "" ,indata.decode('utf-8')))
    155     records = encode(indata, moduleconfig)
    156     outdata = ''
    157     for rec in records: # separate each record with double newline
    158       outdata += rec[0] + '\n' + rec[1] + '\n\n'
    159   else: # decoding flow
    160     # parse the records
    161     rawrecs = indata.decode('utf-8').split('\n\n')
    162     records = [] # records will be stored here
    163     for pairstr in rawrecs:
    164       if len(pairstr) > 0: # exclude empty records
    165         pair = pairstr.split('\n') # get raw pair and then left-adjust
    166         records.append([pair[0].ljust(moduleconfig['namelen'], ' '),
    167                         pair[1].ljust(moduleconfig['numberlen'], ' ')])
    168     # decode the records
    169     outdata = decode(records, moduleconfig, args.expected_length)
    170     if args.type == 'hex': # convert the output data if the type is hex
    171       outdata = outdata.hex()
    172   # now, write the output file
    173   try:
    174     if args.output_file == '-':
    175       outfd = sys.stdout
    176       if type(outdata) == type(b''):
    177         outdata = outdata.decode('utf-8')
    178       outfd.write(outdata)
    179     else:
    180       outfd = open(args.output_file, mode='wb')
    181       if type(outdata) == type(''):
    182         outdata = outdata.encode('utf-8')
    183       outfd.write(outdata)
    184     if outfd != sys.stdout:
    185       outfd.close()
    186   except:
    187     print('Error writing the output file!')
    188     print(sys.exc_info())
    189     exit(1)
    190 
    191   print('\nOperation complete')