• Birinci navigasyona geç
  • Skip to main content

Orçun Başlak

Hayat, teknoloji, enerji ve seyahatlar hakkında notlar

  • Enerji.
  • Seyahat.
  • ORÇUN BAŞLAK.
  • İş Hayatı.
  • Teknoloji.
Evirici çok mu ısındı acaba?

Evirici çok mu ısındı acaba?

25 Ekim 2020

Benim Raspberry Pi cihazlarına olan ilgimi bu blogun okuyucuları biliyor. Yine birgün mühendislik hizmetleri verdiğimiz bir tesiste başımız sıkıştı ve imdadımıza Raspberry yetişti. Nasıl mı? Toplanın anlatıyorum 🙂

Merkezi evirici kullanılan bir sahadayız. Yaz sıcağında güneşin tam tepede olduğu saatlerde gün içerisinde eviricilerde güç kısılması oluyor ama nedenini bulamıyoruz. PR analizlerini gerçekleştirdiğimizde yüksek güçlerde ve sıcak havalarda enerji üretiminde bir düşüş olduğunu saptıyoruz. Bu aşamada raporumuza bunu eksiklik olarak yazıp geçiyoruz ama sorun bir türlü bulunmuyor. Bir gün yine bu konu ile ilgili saçma sapan bir sürü epostada cc’de yer alırken işe el atmaya karar verdim. Bunu anlamanın bir yolu olmalıydı.

“Ah bu cc’yi bulanın gözü kör olsun”

– Zeki Müren

Eviricinin modbus haberleşme listesinde her hata için bir kod tanımlanmıştı. Mevcutta kullanılan izleme sistemi ise bu hata kodlarından hiçbirini almıyordu. Sadece akım, gerilim, güç, sıcaklık vb. gibi bilgileri sistemde izleyebiliyorduk. Evirici de kendi içerisinde geçmiş durum kayıtlarını tutmuyordu. İzleme firması ile sonsuza giden yazışmaların sonucunda bize bu hata kodlarını INTEGER olarak verdiler. 16BIT registerda her BITin farklı bir anlama geldiği durumda bu sayıyı INTEGER olarak vermek nedir? Hata kodunun ne olduğunu anlamak için oturup 2 tabanına çekip sağ baştan bit-bit saymanız gerekiyor sayıyı. Böyle bir çözüm olabilir mi? Lakin bu epostaların birisinde modbus adresleme kitapçığı gözüme ilişti. O anda şimşek çaktı; bunları biz haricen neden okumuyoruz?

Blogumu okuyanlar bilir evde kendi dataloggerim ile enerji tüketimimi takip ediyorum. Düzenli olarak Grafana ve InfluxDB üzerinden evin enerji tüketimini 10sn’lik aralıklarla izlediğim için geçmiş verilerime baktım. Hiç kayıp yoktu -evde elektriğin gittiği günler hariç-. Sonra dedim ki bu sade-minik python kodu bu kadar sağlam çalışabiliyorsa tesiste neden çalışmasın?

Burada dikkat etmeniz gereken kelime “sade”. Benim minimalizm‘e olan takıntımı bilen dostlarım mevcuttur. Aslında tam bu noktada yıllardır süregelen, binlerce kişi tarafından yazılıp test edilmiş Linux gibi bir açık kaynak kodun üzerinde duruyorum; önemli olan işlerin Linux ve sistem araçları tarafından yapılabilecek kısımlarını “ben daha iyi yaparım yeeeaah!” havalarına girmeden onlara delege etmek. Gerisi çorap söküğü.

Linux var, Raspberry var, Python var. Gelin bu helvayı beraber karalım.

– Entegratör 28/m/İstanbul

Önce yine bir Raspberry’e ihtiyacımız var. Ben bu projede RPi3 kullandım ama RPi3 bile bu proje için çok fazla. Pi Zero işinizi görecektir ama Pi Zero’da ethernet çıkışı olmadığı için ben kolayına kaçtım. Daha önce de bahsettiğim Polly Electronics (Türk firması) bu konuda güzel bir shield (ek) geliştirmiş. Onu da kullanabilirsiniz.

View this post on Instagram

A post shared by PE2A (@pollyelectronics)

Zaten bu eklentiyi de Solarify için yapmış gibi duruyorlar. Bilmeyenler için Solarify Türkiye’de bilhassa GES konusunda izleme sistemleri üzerine çalışan genç ve yetenekli bir ekip. Neyse dağıtmadan konumuza dönelim.

Raspberry Debian (linux) üzerinde çalıştığı için “Lite” sürümü çok stabil çalışan bir sürüm. Debian’ı hali hazırda tüm sunucularımda kullanıyorum hayatta da değişmem. Raspbian (artık Raspberry OS oldu adı) ise yıllardır hiç tökezletmedi. Sorun yaşadığım noktalar sadece kendimin pintilik yaptığı ucuz güç kaynağı kullanmak, dandik SD kart kullanmak gibi konular oldu. O zaman ne diyoruz? Raspberry’i endüstriyel uygulamalarda kullanacaksanız;

Raspberry Pi’de SD karttan ve güç kaynağından tasarruf olmaz.

– Raspberry Gazisi

Öncelikle Python ile modbus TCP haberleşmesi yapacaktım. Python doğası gereği aynı işe yarayan pek çok kütüphaneye sahip. Bu aşamada doğru kütüphane ile ilerlemek ileride sizi pek çok dertten kurtarabilir. Python’un kendi içerisinde bir kütüphane olmadığı için harici kütüphane kullanmak zorundaydım. Aradığım cevabı stackoverflow’da buldum. Meraklıları buraya bakabilir. Makale Modbus RTU üzerine ama ben TCP için de geçerli olduğunu düşünüyorum.

Verileri okumamızı sağlayacak modbus kütüphanemizi (modbus-tk) seçtiğimize göre minimal bir kod yazabiliriz. Önce modüler bir modbus haberleşme scripti yazayım diye düşündüm ama modbus haberleşmesinde tek seferde birden fazla register okumak hız açısından önemli. Dışarıdan YAML benzeri bir dosya ile tek tek register okutmak hiç efektif olmuyor o zaman neyi nasıl okuyacağını ben belirleyeyim diye düşündüm. Bu da beni modbus haberleşmesi yazmak yerine o cihaz için bir sürücü yazmaya götürdü (evet aslında bu yaptığıma bir sürücü yazmak denebilir). Evirici için şöyle bir kod çıktı ortaya.

import os
import time
import struct
from datetime import datetime
from collections import OrderedDict
import modbus_tk
import modbus_tk.defines as cst
from modbus_tk import modbus_tcp
import logging
log = logging.getLogger('solarian-datalogger')
DRIVER_NAME = 'ABB_PVS800_TCP'
DRIVER_VERSION = '0.1'
MODBUS_TIMEOUT = 3
TRY_AMOUNT = 10
module_name = os.path.splitext(os.path.basename(__file__))[0]
def get_data(ip_address, port, slave_id, device_name):
"""Instrument class for ABB PVS800 Inverter.
Args:
* ip_address (str): ip address of the TCP device
* port (int): port
Modbus Addresses of the device are as follows:
42496 Heartbeat uint16 0
42497 Inverter main status word 1 bf16
42498 Active power 2 int16 1 kW
42499 Reactive power * 3 int16 1 kVAR
42500 Grid voltage 4 uint16 0.1 V
42501 Grid frequency 5 uint16 0.01 Hz
42502 Power factor 6 int16 0.001
42545 Code of the active fault 0 uint16 1
42546 Main voltage, U-V 1 uint16 0.1 V
42547 Main voltage, V-W 2 uint16 0.1 V
42548 Main voltage W-U 3 uint16 0.1 V
42549 Grid current 4 uint16 0.1 V
42550 DC input voltage 5 uint16 0.1 V
42551 DC bus voltage 6 uint16 0.1 V
42552 DC input current 7 uint16 0.1 A
42553 Grounding current3 8 uint16 1 mA
42554 Isolation resistance3 9 uint16 1 kOhm
42555 Ambient temperature 10 int16 0.1 °C
42556 Highest IGBT temperature, PU1 11 int16 0.1 °C
42557 Highest IGBT temperature, PU21 12 int16 0.1 °C
42558 Highest IGBT temperature, PU31 13 int16 0.1 °C
42559 Highest IGBT temperature, PU41 14 int16 0.1 °C
42560 Control section temperature 15 int16 0.1 °C
42561 UNUSED 16
42562 UNUSED 17
42563 UNUSED 18
42564 Daily kWh supplied2, HIGH ORDER BIT 19 uint16 0.1 kWh
42565 Daily kWh supplied2, LOW ORDER BIT 20 uint16 0.1 kWh -USE THIS-
42566 Total kWh supplied2, HIGH ORDER BIT 21 uint16 1 kWh
42567 Total kWh supplied2, LOW ORDER BIT 22 uint16 1 kWh -USE THIS-
42568 Daily kVAh supplied2, HIGH ORDER BIT 23 uint16 0.1 kVAh
42569 Daily kVAh supplied2, LOW ORDER BIT 24 uint16 0.1 kVAh -USE THIS-
42570 Total kVAh supplied2, HIGH ORDER BIT 25 uint16 1 kVAh
42571 Total kVAh supplied2, LOW ORDER BIT 26 uint16 1 kVAh -USE THIS-
42599 Inverter main Status Word 0 bf16
42600 Limitation status word 1 bf16
42601 UNUSED 2
42602 MPPT Status Word 3 bf16
42603 Grid Status Word 4 bf16
42604 Fan Status Word 5 bf16
42605 UNUSED 6
42606 Environmental Status Word 7 bf16
========= INVERTER MAIN STATUS WORD (42497) =========
0 Ready to switch on
1 Faulted
2 Warning
3 MPPT Enabled
4 Grid Stable
5 DC Voltage within running limits
6 Start inhibited
7 Reduced run
8 Redundant run
9 Q-Compensation
10 Limited
11 Grid Connected
========= LIMITATION STATUS WORD (42600) =========
0 IGBT Temp current limitation
1 P(f) limitation
2 P(U) limitation
3 Grid fault and connect limitation
4 External power limitation
5 FRT recovery limitation
6 Shutdown ramp limitation
7 Power gradient limitation
8 FRT Interaction
9 Ambient temperature current limitation
10 RESERVED
11 Power section temperature limitation
========= MPPT STATUS WORD (42602) =========
0 MPPT Mode (0:LowPower – 1:NormalOperation)
1 Power Limitation Active
2 Minimum voltage limit active
3 Maximum voltage limit active
========= GRID STATUS WORD (42603) =========
0 Undervoltage
1 Overvoltage
2 Underfrequency
3 Overfrequency
4 Anti-Islanding Trip
5 RoCoF Trip
6 Combinatory Trip
7 Moving average trip
8 Zero crossing trip
9 LVRT Trip
10 HVRT Trip
11 External monitor trip
========= FAN STATUS WORD (42604) =========
0 Power Unit 1
1 Power Unit 2
2 Power Unit 3
3 Power Unit 4
4 ISU1 Fan
5 ISU2 Fan
6 Door fan circuit breaker
========= ENVIRONMENTAL STATUS WORD (42606) =========
0 AC busbar thermal protection active
1 DC busbar thermal protection active
2 Cold ambient temp warning
3 Cold ambient temp fault
4 Hot ambient temp warning
5 Hot ambient temp fault
6 IGBT temp warning
7 IGBT temp fault
"""
#Start timer to test for execution time
start_time = time.time()
#Prepare the modbus stack
masterTCP = modbus_tcp.TcpMaster(host=ip_address,port=port)
masterTCP.set_timeout(MODBUS_TIMEOUT)
masterTCP.set_verbose(True)
#Set logging
log.debug('Module: %s – Driver: %s – Reading device %s:%s', module_name, DRIVER_NAME, ip_address,port)
#Create an ordered list to store the values
values = OrderedDict()
#Append devicename and timestamp for InfluxDB
values['Device_Name'] = device_name
values['Date'] = get_timestamp_for_influxdb()
# Read first part
x = 0
while x < TRY_AMOUNT:
try:
read1 = masterTCP.execute(slave_id, cst.READ_HOLDING_REGISTERS, 42496, 7)
log.debug('Module: %s – Read 1 Successful : %s – %s:%s – TRIES:%s', module_name, DRIVER_NAME, ip_address, port, x)
x = TRY_AMOUNT
except Exception as e:
log.error('Module: %s – Read 1 Error : %s – %s:%s – TRIES:%s', module_name, DRIVER_NAME, ip_address, port, x)
x += 1
time.sleep(0.5)
#Read second part
x = 0
while x < TRY_AMOUNT:
try:
read2 = masterTCP.execute(slave_id, cst.READ_HOLDING_REGISTERS, 42545, 27)
log.debug('Module: %s – Read 2 Successful : %s – %s:%s – TRIES:%s', module_name, DRIVER_NAME, ip_address, port, x)
x = TRY_AMOUNT
except Exception as e:
log.error('Module: %s – Read 3 Error : %s – %s:%s – TRIES:%s', module_name, DRIVER_NAME, ip_address, port, x)
x += 1
time.sleep(0.5)
#Read third part
x = 0
while x < TRY_AMOUNT:
try:
read3 = masterTCP.execute(slave_id, cst.READ_HOLDING_REGISTERS, 42599, 8)
log.debug('Module: %s – Read 3 Successful : %s – %s:%s – TRIES:%s', module_name, DRIVER_NAME, ip_address, port, x)
x = TRY_AMOUNT
except Exception as e:
log.error('Module: %s – Read 3 Error : %s – %s:%s – TRIES:%s', module_name, DRIVER_NAME, ip_address, port, x)
x += 1
time.sleep(0.5)
#Parse the data for read 1
values['Active_Power'] = float(signed(read1[2]))
values['Reactive_Power'] = float(signed(read1[3]))
values['Grid_Voltage'] = float(read1[4]) / 10
values['Grid_Frequency'] = float(read1[5]) / 100
values['PowerFactor'] = float(signed(read1[6])) / 1000
#Parse the data for read 2
values['L1_Voltage'] = float(read2[1]) / 10
values['L2_Voltage'] = float(read2[2]) / 10
values['L3_Voltage'] = float(read2[3]) / 10
values['Grid_Current'] = float(read2[4]) / 10
values['DC_Input_Voltage'] = float(read2[5]) / 10
values['DC_Bus_Voltage'] = float(read2[6]) / 10
values['DC_Input_Current'] = float(read2[7]) / 10
values['Grounding_Current'] = float(read2[8]) / 1
values['Isolation_Resistance'] = float(read2[9]) / 1
values['Inverter_Ambient_Temp'] = float(signed(read2[10])) / 10
values['Highest_IGBT_Temp_PU1'] = float(signed(read2[11])) / 10
values['Highest_IGBT_Temp_PU21'] = float(signed(read2[12])) / 10
values['Highest_IGBT_Temp_PU31'] = float(signed(read2[13])) / 10
values['Highest_IGBT_Temp_PU41'] = float(signed(read2[14])) / 10
values['Control_Section_Temp'] = float(signed(read2[15])) / 10
values['Daily_kWh'] = float(read2[20]) / 10
values['Total_kWh'] = float(read2[22]) / 1
values['Daily_kVAh'] = float(read2[24]) / 10
values['Total_kVAh'] = float(read2[26]) / 1
#Extract Status-Limiting-Grid-Env-Fan Words
main_status_word = int(read1[1])
limiting_status_word = int(read3[1])
mppt_status_word = int(read3[3])
grid_status_word = int(read3[4])
fan_status_word = int(read3[5])
environmental_status_word = int(read3[7])
#Extract bits from words MAIN
values['Status_Main_ReadyToSwitchOn'] = float(main_status_word >> 0 & 1)
values['Status_Main_Faulted'] = float(main_status_word >> 1 & 1)
values['Status_Main_Warning'] = float(main_status_word >> 2 & 1)
values['Status_Main_MPPTEnabled'] = float(main_status_word >> 3 & 1)
values['Status_Main_GridStable'] = float(main_status_word >> 4 & 1)
values['Status_Main_DCVoltageWithinLimits'] = float(main_status_word >> 5 & 1)
values['Status_Main_StartInhibited'] = float(main_status_word >> 6 & 1)
values['Status_Main_ReducedRun'] = float(main_status_word >> 7 & 1)
values['Status_Main_RedundantRun'] = float(main_status_word >> 8 & 1)
values['Status_Main_QCompansation'] = float(main_status_word >> 9 & 1)
values['Status_Main_Limited'] = float(main_status_word >> 10 & 1)
values['Status_Main_GridConnected'] = float(main_status_word >> 11 & 1)
#Extract bits from words LIMITING
values['Status_Limiting_IGBTTempCurrentLimitation'] = float(limiting_status_word >> 0 & 1)
values['Status_Limiting_PfLimitation'] = float(limiting_status_word >> 1 & 1)
values['Status_Limiting_PuLimitation'] = float(limiting_status_word >> 2 & 1)
values['Status_Limiting_GridFaultLimitation'] = float(limiting_status_word >> 3 & 1)
values['Status_Limiting_ExternalPowerLimit'] = float(limiting_status_word >> 4 & 1)
values['Status_Limiting_FRTRecoveryLimit'] = float(limiting_status_word >> 5 & 1)
values['Status_Limiting_ShutdownRampLimit'] = float(limiting_status_word >> 6 & 1)
values['Status_Limiting_PowerGradientLimit'] = float(limiting_status_word >> 7 & 1)
values['Status_Limiting_FRTInteraction'] = float(limiting_status_word >> 8 & 1)
values['Status_Limiting_AmbientTempLimitation'] = float(limiting_status_word >> 9 & 1)
values['Status_Limiting_PowerSectionTempLimitation'] = float(limiting_status_word >> 11 & 1)
#Extract bits from words MPPT
values['Status_MPPT_MPPTMode'] = float(mppt_status_word >> 0 & 1)
values['Status_MPPT_PowerLimitationActive'] = float(mppt_status_word >> 1 & 1)
values['Status_MPPT_MinVoltageLimitActive'] = float(mppt_status_word >> 2 & 1)
values['Status_MPPT_MaxVoltageLimitActive'] = float(mppt_status_word >> 3 & 1)
#Extract bits from words GRID
values['Status_Grid_Undervoltage'] = float(grid_status_word >> 0 & 1)
values['Status_Grid_Overvoltage'] = float(grid_status_word >> 1 & 1)
values['Status_Grid_Underfrequency'] = float(grid_status_word >> 2 & 1)
values['Status_Grid_Overfrequency'] = float(grid_status_word >> 3 & 1)
values['Status_Grid_AntiIslandingTrip'] = float(grid_status_word >> 4 & 1)
values['Status_Grid_RoCoFTrip'] = float(grid_status_word >> 5 & 1)
values['Status_Grid_CombinatoryTrip'] = float(grid_status_word >> 6 & 1)
values['Status_Grid_MovingAverageTrip'] = float(grid_status_word >> 7 & 1)
values['Status_Grid_ZeroCrossingTrip'] = float(grid_status_word >> 8 & 1)
values['Status_Grid_LVRTTrip'] = float(grid_status_word >> 9 & 1)
values['Status_Grid_HVRTTrip'] = float(grid_status_word >> 10 & 1)
values['Status_Grid_ExternalMonitorTrip'] = float(grid_status_word >> 11 & 1)
#Extract bits from words FAN
values['Status_Fan_PowerUnit1'] = float(fan_status_word >> 0 & 1)
values['Status_Fan_PowerUnit2'] = float(fan_status_word >> 1 & 1)
values['Status_Fan_PowerUnit3'] = float(fan_status_word >> 2 & 1)
values['Status_Fan_PowerUnit4'] = float(fan_status_word >> 3 & 1)
values['Status_Fan_ISU1Fan'] = float(fan_status_word >> 4 & 1)
values['Status_Fan_ISU2Fan'] = float(fan_status_word >> 5 & 1)
values['Status_Fan_DoorFanCircuitBreaker'] = float(fan_status_word >> 6 & 1)
#Extract bits from words ENVIRONMENT
values['Status_Environment_ACBusbarThermalProtection'] = float(environmental_status_word >> 0 & 1)
values['Status_Environment_DCBusbarThermalProtection'] = float(environmental_status_word >> 1 & 1)
values['Status_Environment_ColdAmbientTempWarning'] = float(environmental_status_word >> 2 & 1)
values['Status_Environment_ColdAmbientTempFault'] = float(environmental_status_word >> 3 & 1)
values['Status_Environment_HotAmbientTempWarning'] = float(environmental_status_word >> 4 & 1)
values['Status_Environment_HotAmbientTempFault'] = float(environmental_status_word >> 5 & 1)
values['Status_Environment_IGBTTempWarning'] = float(environmental_status_word >> 6 & 1)
values['Status_Environment_IGBTTempFault'] = float(environmental_status_word >> 7 & 1)
log.debug('Modbus Scan Completed in : %.4f (DRIVER: %s – UNIT: %s:%s)',(time.time() – start_time),DRIVER_NAME, ip_address, port)
return values
def convert_registers_to_long(start_bit, stop_bit, signed, decimals=0, data=[]):
decimal = {0: 1,1: 10, 2: 100, 3: 1000}
mypack = pack('>HH',data[start_bit],data[stop_bit])
if signed:
format = '>l'
else:
format = '>L'
long_data = unpack(format, mypack)
final_data = float(long_data[0]) / decimal[decimals]
return final_data
def get_timestamp_for_influxdb():
return datetime.utcnow().strftime("%Y-%m-%dT%H:%M:00Z")
def signed(value):
try:
packval = struct.pack('<H',value)
return struct.unpack('<h',packval)[0]
except Exception as e:
log.error("Error in signed-unsigned conversion. "+str(e))
return 0
def get_version():
return DRIVER_NAME+" v"+DRIVER_VERSION
view raw abb_pvs800_tcp_modbustk.py hosted with ❤ by GitHub

Eğer böyle özelleştirilmiş bir yapı değil de genel bir yapı kullansaydım her register için ayrı ayrı okuma gerçekleştirecektim. Bu modelde 3 okuma ile istediğim tüm verileri alabiliyorum. Gerisi dosyanın içerisinde kesip biçme, dönüştürme vb. Eğer ileride başka cihazları da okumam gerekirse hepsi için bir driver yazmam gerekecek ama tek seferde en hızlı şekilde okumuş olacağım. Bu modelin gelecek için daha uygun olduğunu düşünüyorum.

İlk olarak bu kodu yazdığımda nasıl daha hızlı okuyabilirim, nasıl paralelliyebilirim diye düşünmüştüm. Python’da işleri parallelemek için asyncio gibi asenkron çalışmayı sağlayan kütüphaneler var ama bu konulara girmek tek kişiyle yapılacak işler değil. En başında amacımız neydi? İşi sadeleştirmek. Ne kadar az kod, ne kadar çok open-source o kadar iyi.

Eğer ModbusTCP üzerinden farklı cihazlar okuyorsanız aynı programı birlikte 2-3 kere çalıştırıp farklı cihazları aynı anda okumasını sağlayabilirsiniz. Şimdi bu aşamada değinmek istediğim bir durum daha var; uygulamaların sürekli arka planda çalışması. Bizim sektörde datalogger cihazlarında veri okuma yazılımları sürekli arkaplanda çalışır. 1-2 haftadır hiç durmadan çalışan bir yazılımın ne hatalar ile karşılaşacağını öngöremezsiniz. Hata olsa da kolayca test edilebilir değildir ki bizim yaptığımız iş 6 eviriciyi 3sn’de okumak. 1 dakikalık veri aldığınız durumda sadece 3 saniye çalışıyor, 57 saniye hiçbir şey yapmıyoruz demek.

Dizayn aklınızda oluşmaya başladı mı? Basit bir python kodu; 6 evirici için 3 paralel çalışıyor, eviriciyi okuyup sonucu yazıp çıkıyor. Yani kod çalışıyor ve bitiyor kapanıyor. Debug etmeniz gereken süre 3-5sn’lik bir süre. Eğer bu aşamada hata varsa var; yoksa 1-2 hafta hiç durmadan çalışan bir yazılımı debug etmiyorsunuz. Bu işi paralellemek için Debian’da efsane bir shell komutumuz var

PARALLEL’le paralelle.

Şimdi burada 2-3 tane Python yorumlayıcısını paralel çalıştırdığın için fazla sistem kaynağı tüketiyorsun diyebilirsin ama Raspbian ilk açıldığında sadece 30MB bellek kullanıyor. Geriye 895MB kalıyor 🙂 (PiZero’da 418MB) Yani özetle kimseyi kasmadan, yormadan bu iş için fazla fazla kaynağımız var. Burada kilit nokta işlemci kaç çekirdekli ise o kadar paralellemek (RPi3 4 core, PiZero 1 core). Test etmedim ama PiZero’da da belki paralel çalıştırıldığında daha iyi verim alınabilir.

Bu scriptlerin her dakika çalıştırılması için ise Linux Cron’dan daha efektif ne olabilir sizce? Python’da infinite-loop ve scheduler ile artistlikler yapmak yerine yılların Cron’una bu işi versek? Muhteşem oldu. Saat gibi işliyor. Kod tam zamanında çalışıyor; execute ediyor ve çıkıyor. Süper! Gayet minimal.

CRON, İsviçre saatlerine taş çıkartan eski kadim dostumuz.

Şimdi dosyaları okuduk bunları sunucuya göndermemiz gerekli. Öncelikle kısa bir web servisi yazayım diye düşündüm; dosyayı alsın, parse etsin ve veritabanına yazsın ama sürekli bir web sunucunun çalışıp bir web servisi dinlemesi yine minimalist gelmedi. Bu konuyu eldeki imkanlar ile ve mümkünse Debian’ın kendi araçları ile çözmek istedim. Aslında bu konuda gidebileceğim tek bir dostum vardı; FTP sunucu.

Linux’da FTP sunucularda kullanmak üzere yazılmış LFTP diye bir araç var. Basit bir komut geçiyorsunuz ve tüm işlemleri (ve FTP sunucuların tüm nazını buzunu) bu yazılım hallediyor. Eğer Python kullansa idim ftplib kullanmak zorunda kalacaktım (Çok şükür Python içinde halihazırda var) ama bu aşamada oluşacak bir çalışma hatası kodun diğer kısımlarını etkileyebilirdi. Kod neydi? Kod minimalizmdi. Dolayısı ile mikroservis yapısı amacımız. Modbus’u okuyan kod ile FTP’ye dosyaları gönderen servis farklı olmalıydı. Burada da imdadıma LFTP yetişti.

LFTP sen ne güzel bir linux komutusun 🙂

Her saniye yazılan dosyaları kendi sunucuma FTP ile göndermek için de LFTP’yi yine eski dostumuz CRON üzerinden çağırdım. CRON’a her 5 dakikada bir LFTP’yi çalıştırmasını söyledim.

Voila! Dosyalar tıkır tıkır sunucuya gidiyordu; lakin her 5. dakikanın dosyası kayıptı. Acaba neden?

Multithreading’in en önemli problemlerinden birisi yarış durumudur. Bir çekirdekteki işlem diğer çekirdektekinden önce biterse sonuç istemediğimiz bir şey çıkabilir (ki bilgisayarlarımız çoğu zaman tek çekirdek çalışır o konuya şimdi girmeyeceğim) ki bizde de böyle bir durum oldu. 5. dakikada çalışan Modbus kodu daha yazmayı bitirmeden LFTP dosyaları gönderiyor kalanları da siliyordu. 5. dakika da gönderilmeden siliniyordu. Peki iki farklı program arasındaki yarış durumunu nasıl çözecektik? Tabiki yine bir linux komutu ile; FLOCK.

Şişe çevirme oynarken doğruluk mu cesaret mi sorusunun cevabı gelmeden diğer oyuncuya geçmenin önündeki en büyük engel; FLOCK.

Flock adı üstünde file-lock’dan geliyor. Eğer bir programlar silsilesi çalışacaksa bir dosya üzerinde kilit oluşturuyor; o kilit çözülmeden diğer programlar çalışamıyor. Bu aşamada LFTP ve Python Modbus aynı FLOCK dosyasına bağlı. Birisi çalışırsa diğeri çalışmıyor. Öteki çalışıyorsa beriki bekliyor gibi.

Peki ya birisi hiç durmaz ise? Her ne kadar gerek LFTP’nin configürasyon dosyasında, gerek de Python’da “bi dur artık yeter yeaw!” şeklinde korumalar koymuş olsam da bazen işler istediğiniz gibi gitmez. Bir program sürekli çalışıp takılı kalır, diğeri çalışmaz al başına belayı. Zaten cihaz Türkiye’nin diğer ucunda nasıl müdahale edeceksin (Burada da remote + watchdog timer var ama o konulara da şimdi girmiyorum)? Bu durumda karşımıza SIGINT’lerin efendisi güzide bir linux komutumuz çıkıyor; TIMEOUT.

TIMEOUT’u yazanlar demiş ki; okursa okur okumazsa ekime kadar yolu var.

TIMEOUT’da program 30sn’den fazla çalışırsa çak kill -9’u diyorsun; ohh mis. Sistemde hiç takılma olmuyor. Velevki saçma bir konuya takıldı; işte kız arkadaşına bozuldu v.s. basıyor cayırtıyı. Bu da bizim için gayet güzel bir emniyet sibobu.

Farkındaysanız ne kadar farklı hali hazırda bulunan aracı kullandık. Python’u ise sadece modbus okumak ve verileri json şeklinde gzipleyip yazmak için kullandık. Gzip benim ağ trafiğini azaltmak için kullandığım bir sıkıştırma; düz CSV transfer ettiğimizde dosyalar çok büyük oluyor (16KB->2KB). Tabi 16KB büyük mü diyeceksiniz? Elbette değil, mevcut ağlar bunu taşımak için gayet yeterli ama ben burada elde ettiğim boyut farkının bir kısmını TLS’de harcıyorum. Tabi TLS nedir diye soranlar olmuştur. Raspberry konusunu çok dağıtmadan kısaca değineyim; piyasadaki datalogger cihazları internet üzerinde FTP’ye dosya gönderirken tüm haberleşme açık gider gelir. Şifreler açık açık gözükür. Ağı dinleyen kişiler bunu kolayca gözlemleyebilir. Bu konuyu her datalogger/santral için farklı kullanıcı adı/şifre ile çözmeye çalışanlar var ama bu yeterli bir metod değil; önemli olan TLS (transport-layer-security) kullanılması. Siz tabi yine farklı user/pass kombinasyonu ile ekstra bir güvenlik sağlayın. TLS kullandığımızda veriler şifreli kanaldan gittiği için ~%10 gibi bir overhead oluşuyor. O sebeple 8 kat küçülttüğümüz dosya transfer sırasında %10 kadar büyüyebiliyor. Tabi bunlar çok küçük şeyler ama farkında olmak önemli. FTP’de de Let’s Encrypt’den edinilen imzalı sertifikalar kullanmak önemli yoksa verify edemeyebiliyor bazı yazılımlar.

Bu aşamada kod kullandığım tek kısım Modbus okuma kısmı oldu. Ne yazık ki linux araçlarında modbus için sistemle gelen bir araç yok; olsaydı onu kullanırdım. Geriye kalan tüm paketleme, kod çağırma, gönderme işleri linux tarafından hallediliyor. Yıllardır denetlene denetlene gelen yazılım parçacıkları ile. İşte bu açık kaynak kod dünyasının en güzel örneği. Çok minimal bir şekilde bir datalogger sahibi olabiliyorsunuz.

Şöyle bir geriye çekilip baktığınızda ne kadar az kod ile mevcut yazılımları kullanarak ne kadar çok şey yapabiliyoruz değil mi? Bu bence muazzam güzellikte bir durum. Yapmak istediğiniz çoğu şey önceden zaten yapılmış. Önemli olan sizin neyi nerede bulabileceğinizi bilmeniz. 2020 yılında; Github ve stackoverflow’un olduğu bir dünyada -eğer çok spesifik bir talebiniz yoksa- mevcut açık kaynak kodlu yazılımlar sizin pek çok işinizi çözebilir.

Ehhh; bir de ana konumuz vardı, kesintiye uğrayan bir inverter. Ona ne oldu diyenleriniz olabilir (bu kadar teknikten sonra ilk paragrafı hatırlamak da helal), birşey olmadı.

ÇÜNKÜ HAVALAR SOĞUDUĞU VE IŞINIM AZALDIĞI İÇİN INVERTER MEVCUT DURUMDA DA TAM GÜCE ULAŞAMAMAYA BAŞLADI

Şaka gibi değil mi? Ne yazık ki. Neyse, biz önümüzdeki maçlara hazırlanalım.

Fakir ama gururlu. Raspberry Pi’ımız şuanda bir switchin üzerinde ters dönmüş vaziyette eviricileri en ince detayına kadar okuyup bize bildiriyor 🙂
  • Yazdır
  • E-posta
  • Twitter
  • LinkedIn
  • Telegram
  • WhatsApp
  • Daha fazla
  • Pocket
  • Facebook
  • Reddit
  • Pinterest

Orçun Başlak

Teknoloji, hayat ve girişimcilik üzerine.
Takip Edin: Twitter

Okuyucu Etkileşimi

Yorumlar

  1. Kitap aşığı der ki

    25 Ekim 2020 ile 22:44

    Kütüphane mi kaldı diyenlere inat yaşasın kütüphaneli hayat !

    Cevapla
    • Orçun Başlak der ki

      3 Kasım 2020 ile 13:20

      Kütüphane mi kaldı?

      Cevapla

Bir cevap yazın Cevabı iptal et

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

© Tüm Hakları Saklıdır - Orçun Başlak