##| Copyright: (C) 2018-2020 Kevin Larke ##| License: GNU GPL version 3.0 or above. See the accompanying LICENSE file. import os,sys,argparse,yaml,types,select,serial,logging,time,datetime from enum import Enum from multiprocessing import Process, Pipe # Message header id's for messages passed between the application # process and the microcontroller and video processes class TinyOp(Enum): setPwmOp = 0 noteOnVelOp = 1 noteOnUsecOp = 2 noteOffOp = 3 setReadAddr = 4 writeOp = 5 writeTableOp = 6 holdDelayOp = 7 flagsOp = 8 invalidOp = 9 class TinyRegAddr(Enum): kRdRegAddrAddr = 0 kRdTableAddrAddr = 1 kRdEEAddrAddr = 2 kRdSrcAddr = 3 kWrRegAddrAddr = 4 kWrTableAddrAddr = 5 kWrEEAddrAddr = 6 kWrDstAddr = 7 kTmrCoarseAddr = 8 kTmrFineAddr = 9 kTmrPrescaleAddr = 10 kPwmDutyAddr = 11 kPwmFreqAddr = 12 kPwmDivAddr = 13 kStateAddr = 14 kErrorCodeAddr = 15 kMaxAllowTmrAddr = 16 kDelayCoarseAddr = 17 kDelayFineAddr = 18 kFlagsAddr = 19 class TinyRegAddr0(Enum): kRdRegAddrAddr = 0 kRdSrcAddr = 1 kWrRegAddrAddr = 2 kWrDstAddr = 3 kAttkDutyAddr = 4 kAttkDurHiAddr = 5 kAttkDurLoAddr = 6 kDecayStepAddr = 7 kDecayDecrAddr = 8 kPwmDutyAddr = 9 kErrorCodeAddr = 10 class TinyConst(Enum): kRdRegSrcId = TinyRegAddr.kRdRegAddrAddr.value # 0 kRdTableSrcId = TinyRegAddr.kRdTableAddrAddr.value # 1 kRdEESrcId = TinyRegAddr.kRdEEAddrAddr.value # 2 kWrRegDstId = TinyRegAddr.kWrRegAddrAddr.value # 4 kWrTableDstId = TinyRegAddr.kWrTableAddrAddr.value # 5 kWrEEDstId = TinyRegAddr.kWrEEAddrAddr.value # 6 kWrAddrFl = 0x08 # first avail bit above kWrEEAddr class SerialMsgId(Enum): QUIT_MSG = 0xffff DATA_MSG = 0xfffe class Result(object): def __init__( self, value=None, msg=None ): self.value = value self.msg = msg def set_error( self, msg ): if self.msg is None: self.msg = "" self.msg += " " + msg def __bool__( self ): return self.msg is None def _serial_process_func( serial_dev, baud, pipe ): reset_N = 0 drop_N = 0 noSync_N = 0 with serial.Serial(serial_dev, baud) as port: while True: # get the count of available bytes in the serial port buffer bytes_waiting_N = port.in_waiting # if no serial port bytes are available then sleep .... if bytes_waiting_N == 0: time.sleep(0.01) # ... for 10 ms else: # read the serial port ... v = port.read(bytes_waiting_N) pipe.send((SerialMsgId.DATA_MSG,v)) # ... and send it to the parent msg = None if pipe.poll(): # non-blocking check for parent process messages try: msg = pipe.recv() except EOFError: break # if an incoming message was received if msg != None: # this is a shutdown msg if msg[0] == SerialMsgId.QUIT_MSG: pipe.send(msg) # ... send quit msg back break # this is a data xmit msg elif msg[0] == SerialMsgId.DATA_MSG: port.write(msg[1]) class SerialProcess(Process): def __init__(self,serial_dev,serial_baud): self.parent_end, child_end = Pipe() super(SerialProcess, self).__init__(target=_serial_process_func, name="Serial", args=(serial_dev,serial_baud,child_end,)) self.doneFl = False def quit(self): # send quit msg to the child process self.parent_end.send((SerialMsgId.QUIT_MSG,0)) def send(self,msg_id,value): # send a msg to the child process self.parent_end.send((msg_id,value)) return Result() def recv(self): # x = None if not self.doneFl and self.parent_end.poll(): x = self.parent_end.recv() if x[0] == SerialMsgId.QUIT_MSG: self.doneFl = True return x def is_done(self): return self.doneFl class Picadae: def __init__( self, key_mapL, i2c_base_addr=21, serial_dev='/dev/ttyACM0', serial_baud=38400, prescaler_usec=16 ): """ key_mapL = [{ index, board, ch, type, midi, class }] serial_dev = /dev/ttyACM0 serial_baud = 38400 i2c_base_addr = 1 """ self.serialProc = SerialProcess( serial_dev, serial_baud ) self.keyMapD = { d['midi']:d for d in key_mapL } self.i2c_base_addr = i2c_base_addr self.prescaler_usec = prescaler_usec self.log_level = 0 self.serialProc.start() def close( self ): self.serialProc.quit() def wait_for_serial_sync(self, timeoutMs=10000): # wait for the letter 'a' to come back from the serial port result = self.block_on_serial_read(1,timeoutMs) if result and len(result.value)>0 and result.value[0] == ord('a'): pass else: result.set_error("Serial sync failed.") return result def write_tiny_reg( self, i2c_addr, reg_addr, byteL ): return self._send( 'w', i2c_addr, reg_addr, [ len(byteL) ] + byteL ) def call_op( self, midi_pitch, op_code, argL ): return self.write_tiny_reg( self._pitch_to_i2c_addr( midi_pitch ), op_code, argL ) def set_read_addr( self, i2c_addr, mem_id, addr ): # mem_id: 0=reg_array 1=vel_table 2=eeprom return self.write_tiny_reg(i2c_addr, TinyOp.setReadAddr.value,[ mem_id, addr ]) def read_request( self, i2c_addr, reg_addr, byteOutN ): return self._send( 'r', i2c_addr, reg_addr,[ byteOutN ] ) def block_on_serial_read( self, byteOutN, time_out_ms=250 ): ts = datetime.datetime.now() + datetime.timedelta(milliseconds=time_out_ms) retL = [] while datetime.datetime.now() < ts and len(retL) < byteOutN: # If a value is available at the serial port return is otherwise return None. x = self.serialProc.recv() if x is not None and x[0] == SerialMsgId.DATA_MSG: for b in x[1]: retL.append(int(b)) time.sleep(0.01) result = Result(value=retL) if len(retL) < byteOutN: result.set_error("Serial port time out on read.") return result def block_on_picadae_read( self, midi_pitch, mem_id, reg_addr, byteOutN, time_out_ms=250 ): i2c_addr = self._pitch_to_i2c_addr( midi_pitch ) result = self.set_read_addr( i2c_addr, mem_id, reg_addr ) if result: result = self.read_request( i2c_addr, TinyOp.setReadAddr.value, byteOutN ) if result: result = self.block_on_serial_read( byteOutN, time_out_ms ) return result def block_on_picadae_read_reg( self, midi_pitch, reg_addr, byteOutN=1, time_out_ms=250 ): return self.block_on_picadae_read( midi_pitch, TinyRegAddr.kRdRegAddrAddr.value, reg_addr, byteOutN, time_out_ms ) def note_on_vel( self, midi_pitch, midi_vel ): return self.call_op( midi_pitch, TinyOp.noteOnVelOp.value, [self._validate_vel(midi_vel)] ) def note_on_us( self, midi_pitch, pulse_usec ): return self.call_op( midi_pitch, TinyOp.noteOnUsecOp.value, list(self._usec_to_coarse_and_fine(pulse_usec)) ) def note_off( self, midi_pitch ): return self.call_op( midi_pitch, TinyOp.noteOffOp.value, [0] ) # TODO: sending a dummy byte because we can't handle sending a command with no data bytes. def set_hold_delay( self, midi_pitch, pulse_usec ): return self.call_op( midi_pitch, TinyOp.holdDelayOp.value, list(self._usec_to_coarse_and_fine(pulse_usec)) ) def get_hold_delay( self, midi_pitch, time_out_ms=250 ): res = self.block_on_picadae_read_reg( midi_pitch, TinyRegAddr.kDelayCoarseAddr.value, byteOutN=2, time_out_ms=time_out_ms ) if len(res.value) == 2: res.value = [ self.prescaler_usec*255*res.value[0] + self.prescaler_usec*res.value[1] ] return res def set_flags( self, midi_pitch, flags ): return self.call_op( midi_pitch, TinyOp.flagsOp.value, [int(flags)] ) #return self.call_op( midi_pitch, 5, [int(flags)] ) def get_flags( self, midi_pitch, time_out_ms=250 ): return self.block_on_picadae_read_reg( midi_pitch, TinyRegAddr.kFlagsAddr.value, byteOutN=1, time_out_ms=time_out_ms ) #return self.block_on_picadae_read_reg( midi_pitch, TinyRegAddr.kAttkDutyAddr.value, byteOutN=1, time_out_ms=time_out_ms ) def set_velocity_map( self, midi_pitch, midi_vel, pulse_usec ): coarse,fine = self._usec_to_coarse_and_fine( pulse_usec ) src = TinyConst.kWrAddrFl.value | TinyConst.kWrTableDstId.value addr = midi_vel*2 return self.call_op( midi_pitch, TinyOp.writeOp.value, [ src, addr, coarse, fine ] ) def get_velocity_map( self, midi_pitch, midi_vel, time_out_ms=250 ): byteOutN = 2 return self.block_on_picadae_read( midi_pitch, TinyConst.kRdTableSrcId.value, midi_vel*2, byteOutN, time_out_ms ) def set_pwm_duty( self, midi_pitch, duty_cycle_pct ): if 0 <= duty_cycle_pct and duty_cycle_pct <= 100: # duty_cycle_pct = 100.0 - duty_cycle_pct return self.call_op( midi_pitch, TinyOp.setPwmOp.value, [ int( duty_cycle_pct * 255.0 /100.0 )]) else: return Result(msg="Duty cycle (%f) out of range 0-100." % (duty_cycle_pct)) def get_pwm_duty( self, midi_pitch, time_out_ms=250 ): return self.block_on_picadae_read_reg( midi_pitch, TinyRegAddr.kPwmDutyAddr.value, time_out_ms=time_out_ms ) def set_pwm_freq( self, midi_pitch, freq ): res = self.get_pwm_duty( midi_pitch ) if res: print("duty",int(res.value[0])) res = self.call_op( midi_pitch, TinyOp.setPwmOp.value, [ int(res.value[0]), int(freq) ]) return res def get_pwm_freq( self, midi_pitch, time_out_ms=250 ): return self.block_on_picadae_read_reg( midi_pitch, TinyRegAddr.kPwmFreqAddr.value, time_out_ms=time_out_ms ) def get_pwm_div( self, midi_pitch, time_out_ms=250 ): return self.block_on_picadae_read_reg( midi_pitch, TinyRegAddr.kPwmDivAddr.value, time_out_ms=time_out_ms ) def set_pwm_div( self, midi_pitch, div, time_out_ms=250 ): res = self.get_pwm_duty( midi_pitch ) if res: duty = res.value[0] res = self.get_pwm_freq( midi_pitch ) if res: res = self.call_op( midi_pitch, TinyOp.setPwmOp.value, [ int(duty), int(res.value[0]), int(div) ]) return res def write_table( self, midi_pitch, time_out_ms=250 ): # TODO: sending a dummy byte because we can't handle sending a command with no data bytes. return self.call_op( midi_pitch, TinyOp.writeTableOp.value,[0]) def make_note( self, midi_pitch, atk_us, dur_ms ): # TODO: handle error on note_on_us() self.note_on_us(midi_pitch, atk_us); time.sleep( dur_ms / 1000.0 ) return self.note_off(midi_pitch) def make_seq( self, midi_pitch, base_atk_us, dur_ms, delta_us, note_cnt ): for i in range(note_cnt): self.make_note( midi_pitch, base_atk_us + i*delta_us, dur_ms ) time.sleep( dur_ms / 1000.0 ) return Result() def make_scale( self, pitch0, pitch1, atk_us, dur_ms ): if pitch0>pitch1: printf("pitch0 must be <= pitch1") else: for pitch in range(pitch0,pitch1+1): self.make_note( pitch, atk_us, dur_ms ) time.sleep( dur_ms / 1000.0 ) return Result() def set_log_level( self, log_level ): self.log_level = log_level return Result() def _pitch_to_i2c_addr( self, pitch ): return self.keyMapD[ pitch ]['index'] + self.i2c_base_addr def _validate_vel( self, vel ): return vel def _usec_to_coarse_and_fine( self, usec ): coarse_usec = self.prescaler_usec*255 # usec's in one coarse tick coarse = int( usec / coarse_usec ) fine = int(round((usec - coarse*coarse_usec) / self.prescaler_usec)) assert( coarse <= 255 ) assert( fine <= 255) x = coarse*coarse_usec + fine*self.prescaler_usec ##### # n = int(16e6*usec/(256*1e6)) # coarse = n >> 8; # fine = n & 0xff; # x = usec #### print("C:%i F:%i : %i %i (%i)" % (coarse,fine, x, usec, usec-x )) return coarse,fine def _send( self, opcode, i2c_addr, reg_addr, byteL ): self._print( opcode, i2c_addr, reg_addr, byteL ) byteA = bytearray( [ord(opcode), i2c_addr, reg_addr ] + byteL ) return self.serialProc.send(SerialMsgId.DATA_MSG, byteA ) def _print( self, opcode, i2c_addr, reg_addr, byteL ): if self.log_level: s = "{} {} {}".format( opcode, i2c_addr, reg_addr ) for x in byteL: s += " {}".format(x) print(s)