"""
Program     : Percobaan Kontroler NN menggunakan Micro Python ESP32
DiBuat Oleh : EruP-RMK
Keluaran    : v1.00
Tanggal     : 01 Juli 2005 - NN Versi MCS52
              05 Desember 2008 - NN Versi AVR ATmega8
              30 Oktober 2019 - NN Versi Arduino
              12 September 2018 - 8 Agustus 2020 - Observasi Plant
              17 Nopember 2020 - 19 Nopember 2020 - NN + Observasi Plant
              30 Januari 2024 - 31 Januari 2024 - Micro Python ESP32
Untuk       : Praktikum Kontrol Cerdas 2 POLITEKNIK ELEKTRONIKA NEGERI SURABAYA (PENS)

Perangkat   : Arduino Mega, Plant Emulasi, Koneksi ADC Emulasi dan Pin Port I/O (sebagai PWM) Emulasi
"""

import math
import random
import machine
import time

from ili9341 import Display, color565
from machine import Pin, SPI

spi = SPI(2, baudrate=32000000, sck=Pin(18), mosi=Pin(23), miso=Pin(19))
display = Display(spi, dc=Pin(33), cs=Pin(5), rst=Pin(32))
line=display.draw_line
cls=display.clear
RGB=color565

MinX=0
MinY=0
MaxX=239
MaxY=319

import tm1637

ss = tm1637.TM1637(clk=Pin(4), dio=Pin(2))


#line(0,0,200,100, color565(0,0,255))
#line(0,0,100,100, color565(0,255,0))
#line(0,0,100,200, color565(0,255,255))
#line(0,0,200,200, color565(255,255,0))
#line(MinX,MinY,MaxX,MaxY, RGB(255,255,255))

"""
v=0
x=0
y=0
i=0
while True:
  v+=(12-v)*0.01/(10*0.05)         # V = IT/C
  #print(v)
  line(239-x,int(y),238-x,int(v*10), RGB(255,0,0))
  x+=1
  if x>=239:
    x=0
    cls()
  y=v*10
  #sleep(1)
"""

uart=machine.UART(1, baudrate=115200, tx=16, rx=17)

MOTOR=3                                # Nomor PIN untuk output PWM (emulasi PWM oleh Arduino)
SENSOR=34#A0                              # Nomot PIN ADC

Kecepatan=1000
SettingPoint=1000
PWM=0
adc=1
Load=0                                     # Pembebanan, 0% = tanpa beban, 100% = beban maksimal
Periode=0                                  # panjang periode (Setting Point on/off) dalam satuan sampling, Periode=0 -> tidak periodik
Random=0                                   # nilai Setting Point acak
Index=0                                    # penghitung sampling (Loop)
SetP=1000                                     # BackUp Setting Point
RpmMax=2000                                # Display Max
Error=0
MSE=2e5

VinMin=0.25                             # Tegangan plant minimal, antara 0 volt sampai 5 volt
alfa=0.1                                # semakin kecil Alfa, TM semakin besar
KM=2000/5                               # konversi dari tegangan ke RPM
v1=0
v2=0
def plant(vin):                         # Plant Emulasi
  global v1, v2
  vin-=VinMin
  if vin<0: vin=0                       # dilengkapi dengan titik nyala minimal
  v1=vin*.7+v1*.3                       # orde #1
  v2=v1*alfa+v2*(1-alfa)                # orde #2
  return v2*KM                          # konversi dari tegangan ke RPM

out_plant=0
def _analogRead(ch):                    # ADC Emulasi
  return out_plant/KM*1023/5*(1.0-Load) # VRef 5 Volt resolusi 10 bit

def _analogWrite(pin, PWM):             # DAC Emulasi
  global out_plant
  out_plant = plant(PWM*5.0/255)        # VRef 5 Volt resolusi 8 bit

"""
Demo NN, SImple NN dengan
- 1 Input Node
- 1 LApis Hidden Layer, 5-beberapa Node
- 1 Output Neuron
- Domain Data [0,1]
"""

MaxHidden=50                              # Max Jumlah Node/Neuron pada Hidden Layer
LearningRate = 1                           # dapat diubah dengan perintah "R0.5"
nHidden = 5                                  # dapat diubah dengan perintah "H10"

InNN=0                                       # Data Masukan NN, 1 input
OutNN=0                                      # Data Keluaran NN, Output Neouron, setelah fungsi Aktifasi, 1 output
NH=[0]*MaxHidden                              # Keluaran Hidden Neuron, setelah Aktifasi
eH=[0]*MaxHidden                              # Error Propagasi Hidden Neuron, setelah Aktifasi
BH=[0]*MaxHidden                              # Bias dari Hidden Neuron
WH=[0]*MaxHidden                              # Bobot Masukan Hidden Neuron
WO=[0]*MaxHidden                              # Bobot Masukan dari Output Neuron
BO=0                                         # Bias Output Neuron
ErrNN=0                                      # Error untuk sistem update NN

def Init_NN():
  global WH, BH, WO, BO
  for h in range(MaxHidden):
    WH[h]=random.uniform(-1,1)                # Bangkitkan random untuk tiap bobot, [-1,1]
    BH[h]=0                                      # Nilai Awal Bias=0
    WO[h]=random.uniform(-1,1)                # Bangkitkan random untuk tiap bobot, [-1,1]
  BO=0

def sigmoid(x):
  if x>5:
    return 1                               # Untuk menghindarkan OverFlow,
  elif x<-5:
    return 0                         # Jika x>5 set y=1, Jika x<5, y=0
  else:
    return 1/(1+math.exp(-x))               # Diantara itu, hitung normal

def HitungMaju():
  global nHidden, InNN, WH, BH, NH, WO, BO, OutNN
  zo=0
  for h in range(nHidden):
    zh=InNN*WH[h]+BH[h]
    NH[h]=sigmoid(zh)                            # Hitung keluaran masing-masing Hidden Neuron  
    zo+=NH[h]*WO[h]                              # Sekaligus hitung masukan Sigma dari Output Neuron
  OutNN=sigmoid(zo+BO)                           # Hitung keluaran dari Output Neuron

def UpdateNN():
  #unsigned char h
  #float eo, eh
  global eH, WO, WH, BH, BO, ErrNN, OutNN, nHidden
  global LearningRate, NH, InNN
  eo=ErrNN*OutNN*(1-OutNN)                       # temporary, biar cepat
  for h in range(nHidden):
    eH[h]=WO[h]*eo                               # selesaikan sebelum update WO
    WO[h]+=LearningRate*eo*NH[h]                 # update bobot output
    eh=eH[h]*NH[h]*(1-NH[h])                     # temporary, biar cepat
    WH[h]+=LearningRate*eh*InNN
    BH[h]+=LearningRate*eh
  BO+=LearningRate*eo

#randomSeed(analogRead(0))                      # atur nilai awal bilangan random, dari ADC
Init_NN()


x=0
SP=0        # Setting Point lalu
K=0         # Kecepatan lalu
P=0         # PWM lalu
E=0         # Error lalu
M=0         # MSE lalu

def run():
  while True:                                       # Bagian yang dikerjakan secara terus-menerus oleh Arduino
    if uart.any():                          # Apakah ada penerimaan data serial dari PC ?
      uart.read(d)
      if d=='s':                                   # Jika Ya, apakah data dimulai dengan huruf depan 's' ?
        SetP=Serial.parseInt()                   # Ubah Setting Point, "s1000"
        if(SetP<0): SetP=0
        elif(SetP>RpmMax): SetP=RpmMax
        Index=0
      elif d=='a':                                   # Ubah Timing plant, "a0.0" - "a1.0" alfa 0.00 sampai alfa 1.00
        alfa=Serial.parseFloat()
        if(alfa<0): alfa=0
        elif(alfa>1): alfa=1
      elif d=='l':                                   # Ubah beban Plant, "l0" - "l100" load mati (0%) sampai load 100%
        Load=Serial.parseInt()/100.0
        if(Load<0): Load=0
        elif(Load>1): Load=1
      elif d=='p':                                   # 'p0' periodik mati, 'p100' periodik setiap 100 siklus (sampling)
        Periode=Serial.parseInt()
        Periode=(Periode/2)*2                    # harus genap
        if(Periode<0): Periode=0
        elif(Periode>1000): Periode=1000
        Index=0
      elif d=='r':                                   # 'r0' random mati, 'r1' random aktif
        Random=Serial.parseInt()
        if(Random<0): Random=0
        elif(Random>1): Random=1
        randomSeed(analogRead(0))
      elif d=='v':                                   # 'v0' ... 'v5' nilai Vmin dari 0 volt sampai 5 volt
        VinMin=Serial.parseFloat()
        if(VinMin<0): VinMin=0
        elif(VinMin>5): VinMin=5
      elif d=='m':                                   # 'r0' random mati, 'r1' random aktif
        RpmMax=Serial.parseInt()
        if(RpmMax<0): RpmMax=0
        elif(RpmMax>10000): RpmMax=10000
      elif d=='R':                                   # Ubah Learning Rate, "R0.0" - "R10.0", jangan terlalu besar
        LearningRate=Serial.parseFloat()
        if(LearningRate<0): LearningRate=0
      elif d=='H':                                   # Ubah Jumlah Neuron pada Hidden Layer, "H1" - "H50"
        nHidden=Serial.parseInt()
        if(nHidden<0): nHidden=0
        elif(nHidden>MaxHidden): nHidden=MaxHidden
      elif d=='I':                                   # Inisialisasi ulang NN, "I"
        Init_NN()
    
                                                 # Misal data yang diterima, "s1000"<CR>
                                                  # Set Carriage Return pada terminal, agar saat enter, lansung dikirimkan CR  
    if(Index==0):
      if Random==0: SettingPoint=SetP
      else: SettinPoint=random(0,RpmMax)
    if(Periode>0 and Index*2==Periode):
      if Random==0: SettingPoint=0
      else: SettingPoint=random(0,RpmMax)

    adc=_analogRead(SENSOR)                        # Baca tegangan sensor dari PORT ADC, tegangan sensor sebanding dengan kecepatan/keluaran PLANT
    Kecepatan=adc*RpmMax/1023                      # Konversi dari ADC ke Kecepatan (dari data biner ADC ke RPM)
  
    Error=SettingPoint-Kecepatan
    MSE=Error*Error*0.02+MSE*0.98
  
    #InNN=SettingPoint/RpmMax                     # Open Loop, InNN=[0,1], diambil dari SettingPoint=[0,2000]
    InNN=Error/(2*RpmMax)+0.5                      # CloseLoop, InNN=[0,1], diambil dari Error=[-2000,2000]
    #InNN=Kecepatan/RpmMax                        # Recurrent, InNN dari output Kecepatan
  
    HitungMaju()                                   # Hitung dari InNN[0,1] ke OutNN[0,1]
  
    ErrNN=Error/RpmMax                             # Error untuk Update NN, nilai [-1,1], dari Error[-2000,2000]
    UpdateNN()                                     # Dari ErrNN, lakukan Update secara OnLine

    PWM=OutNN*255                                  # Tentukan nilai PWM (data biner) berdasarkan Setting Point yang telah ditentukan
    if(PWM<0): PWM=0
    if(PWM>255): PWM=255
    _analogWrite(MOTOR,PWM)                        # Kirim ke PLANT melalui PORT I/O (sinyal PWM yang di-emulasi dalam bentuk program dalam Arduino)
 
    print("SetPoint=", SettingPoint,sep='', end=' ')                      # Tampilkan SetPoint ke Keterangan, SetPoint=1000
    print(" Kecepatan=", Kecepatan,sep='', end=' ')                    # Tampilkan Kecepatan
    print(" PWM=", PWM,sep='', end=' ')
    print(" Error=", Error,sep='', end=' ')
    print(" MSE=", MSE,sep='', end=' ')
    print(" Max=", RpmMax,sep='', end=' ')
    print(" Hidden=", nHidden,sep='', end=' ')
    print(" LR=", LearningRate, sep='')
 

    #print(SettingPoint,sep='', end=' ')                   # Kirim data ke program monitor melalui komunikasi serial
    #print(Kecepatan,sep='', end=' ')                      # Format Protokol: "1000 850 87 2000"<CR>
    #print(PWM,sep='', end=' ')                            # tanda 0 artinya tidak ada nilai pecahan
    #if Error>RpmMax: print(RpmMax,sep='', end=' ')
    #elif Error<0: print(0,sep='', end=' ')
    #else: print(Error,sep='', end=' ')
    #if MSE/100>RpmMax: print(RpmMax,sep='', end=' ')
    #else: print(MSE/1000,sep='', end=' ')
    #print(RpmMax)                         # akhir data serial, dengan karakter <LF-CR>

    ss.number(int(Kecepatan))

    if x<239:
      line(239-x,int(SP),238-x,int(SettingPoint/10), RGB(0,0,255))
      SP=SettingPoint/10
      line(239-x,int(K),238-x,int(Kecepatan/10), RGB(255,0,0))
      K=Kecepatan/10
      line(239-x,int(P),238-x,int(PWM), RGB(0,255,0))
      P=PWM
      line(239-x,int(E),238-x,int(Error/10), RGB(0,255,255))
      E=Error/10
      line(239-x,int(M),238-x,int(MSE/1000), RGB(255,255,0))
      M=MSE/1000
      x+=1

    if(Periode>0):                                   # Jika setting point dibuat periodik
      Index+=1
      if(Index>=Periode): Index=0
  
    time.sleep_ms(10)                                      # Atur waktu sampling

run()
esp:0
esp:2
esp:4
esp:5
esp:12
esp:13
esp:14
esp:15
esp:16
esp:17
esp:18
esp:19
esp:21
esp:22
esp:23
esp:25
esp:26
esp:27
esp:32
esp:33
esp:34
esp:35
esp:3V3
esp:EN
esp:VP
esp:VN
esp:GND.1
esp:D2
esp:D3
esp:CMD
esp:5V
esp:GND.2
esp:TX
esp:RX
esp:GND.3
esp:D1
esp:D0
esp:CLK
lcd1:VCC
lcd1:GND
lcd1:CS
lcd1:RST
lcd1:D/C
lcd1:MOSI
lcd1:SCK
lcd1:LED
lcd1:MISO
lcd1:SCL
lcd1:SDA
4-Digit Display
sevseg1:CLK
sevseg1:DIO
sevseg1:VCC
sevseg1:GND