5 Cara Membuat Server Python di Raspberry Pi Bagian 2

Hai, Habr.

Hari ini kita akan terus mempelajari kemampuan jaringan Raspberry Pi, atau lebih tepatnya implementasinya dalam Python. Pada bagian pertama, kami memeriksa fungsi-fungsi dasar dari server web paling sederhana yang berjalan pada Raspberry Pi. Sekarang kita akan melangkah lebih jauh, dan mempertimbangkan beberapa cara untuk membuat server kami interaktif.



Artikel ini dirancang untuk pemula.

Sebelum Anda memulai beberapa catatan.

Pertama-tama, saya sendiri ragu apakah akan melanjutkan dan mengharapkan aliran kritik yang lebih besar dan peringkat yang rendah, tetapi seperti yang ditunjukkan oleh survei di bagian pertama, 85% pembaca berpendapat bahwa informasi yang disediakan di sana bermanfaat. Saya mengerti bahwa beberapa artikel pro untuk "boneka" itu menyebalkan, tetapi semuanya dimulai sekali, jadi Anda harus menunggu.

Kedua, saya akan menulis tentang pemrograman, bukan tentang administrasi. Jadi masalah konfigurasi Raspbian, konfigurasi, VPN, keamanan, dan hal-hal lain tidak akan dipertimbangkan di sini. Meskipun ini juga penting, seseorang tidak dapat merangkul yang besar. Ini hanya tentang Python, dan cara membuat server di atasnya.

Mereka yang tidak tertarik dapat mengklik tombol kembali di browser sekarang dan tidak membuang waktu berharga mereka;)

Dan kita akan mulai.

Biarkan saya mengingatkan Anda bahwa pada bagian sebelumnya kami akhirnya meluncurkan server web sederhana pada Raspberry Pi, menampilkan halaman statis:

gambar

Sekarang kita akan melangkah lebih jauh dan membuat server kami interaktif, tambahkan kontrol LED ke halaman web. Tentu saja, alih-alih LED, mungkin ada perangkat lain yang dapat dikendalikan oleh GPIO, tetapi dengan LED itu yang paling mudah untuk melakukan percobaan.

Persiapan


Saya tidak akan menjelaskan cara menghubungkan LED ke Raspberry Pi, siapa pun dapat menemukannya di Google dalam 5 menit. Kami akan menulis beberapa fungsi untuk menggunakan GPIO sekaligus, yang kemudian kami masukkan ke server kami.

try: import RPi.GPIO as GPIO except ModuleNotFoundError: pass led_pin = 21 def raspberrypi_init(): try: GPIO.setmode(GPIO.BCM) GPIO.setup(led_pin, GPIO.OUT) except: pass def rasperrypi_pinout(pin: int, value: bool): print("LED ON" if value else "LED OFF") try: GPIO.output(pin, value) except: pass def rasperrypi_cleanup(): try: GPIO.cleanup() except: pass 

Seperti yang Anda lihat, setiap fungsi panggilan GPIO "dibungkus" dalam blok coba-tangkap. Mengapa ini dilakukan? Ini memungkinkan Anda untuk men-debug server pada PC apa pun, termasuk Windows, yang cukup nyaman. Sekarang kita bisa memasukkan fungsi-fungsi ini ke dalam kode server web.

Tugas kami adalah menambahkan tombol ke halaman web yang memungkinkan kami untuk mengontrol LED dari browser. 3 cara implementasi akan dipertimbangkan.

Metode 1: Salah


Metode ini tidak bisa disebut cantik, tetapi metode ini pendek dan paling mudah dipahami.

Buat garis dengan halaman HTML.

 html = '''<html> <style>html{font-family: Helvetica; display:inline-block; margin: 0px auto; text-align: center;} .button_led {display: inline-block; background-color: #e7bd3b; border: none; border-radius: 4px; color: white; padding: 16px 40px; text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;} </style> <body> <h2>Hello from the Raspberry Pi!</h2> <p><a href="/led/on"><button class="button button_led">Led ON</button></a></p> <p><a href="/led/off"><button class="button button_led">Led OFF</button></a></p> </body> </html>''' 

Ada 3 poin yang perlu diperhatikan di sini:

  • Kami menggunakan CSS untuk menentukan gaya tombol. Anda tidak bisa melakukan ini dan bergaul dengan hanya 4 baris kode HTML, tetapi kemudian halaman kami akan terlihat seperti "salam dari tahun 90-an":

  • Untuk setiap tombol, kami membuat tautan lokal seperti / led / on dan / led / off
  • Mencampur sumber daya dan kode adalah gaya pemrograman yang buruk, dan idealnya, HTML sebaiknya dipisahkan dari kode Python. Tetapi tujuan saya adalah untuk menunjukkan kode yang berfungsi minimal di mana ada minimum berlebihan, sehingga beberapa hal dihilangkan untuk kesederhanaan. Selain itu, akan lebih mudah bila kode hanya dapat disalin dari sebuah artikel, tanpa kerepotan dengan file tambahan.

Kami sudah memeriksa server di bagian sebelumnya, masih menambahkan proses '/ led / on' dan '/ led / off' untuk itu. Seluruh kode diperbarui:

 from http.server import BaseHTTPRequestHandler, HTTPServer class ServerHandler(BaseHTTPRequestHandler): def do_GET(self): print("GET request, Path:", self.path) if self.path == "/" or self.path.endswith("/led/on") or self.path.endswith("/led/off"): if self.path.endswith("/led/on"): rasperrypi_pinout(led_pin, True) if self.path.endswith("/led/off"): rasperrypi_pinout(led_pin, False) self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(html.encode('utf-8')) else: self.send_error(404, "Page Not Found {}".format(self.path)) def server_thread(port): server_address = ('', port) httpd = HTTPServer(server_address, ServerHandler) try: httpd.serve_forever() except KeyboardInterrupt: pass httpd.server_close() if __name__ == '__main__': port = 8000 print("Starting server at port %d" % port) raspberrypi_init() server_thread(port) rasperrypi_cleanup() 

Kami memulainya, dan jika semuanya dilakukan dengan benar, maka kami dapat mengontrol LED melalui server web kami:



Anda dapat menguji server tidak hanya pada Raspberry Pi, tetapi juga pada Windows atau OSX, di konsol akan ada pesan LED ON, LED OFF ketika Anda mengklik tombol yang sesuai:



Sekarang kita mencari tahu mengapa metode ini buruk, dan mengapa itu "salah." Contoh ini cukup berhasil, dan cukup sering disalin dalam berbagai tutorial. Tetapi ada dua masalah - pertama, salah memuat ulang seluruh halaman saat kami hanya ingin menyalakan LED. Tapi ini masih setengah dari masalah. Masalah kedua, dan yang lebih serius, adalah ketika kita menekan tombol untuk menyalakan LED, alamat halaman menjadi http://192.168.1.106:8000/led/on . Peramban biasanya mengingat halaman yang terakhir dibuka, dan saat berikutnya Anda membuka peramban, perintah untuk menyalakan LED akan bekerja lagi, bahkan jika kami tidak mau. Karena itu, kita akan beralih ke cara selanjutnya yang lebih benar.

Metode 2: Benar


Untuk melakukan semuanya dengan benar, kami mematikan dan mematikan fungsi LED dalam permintaan terpisah, dan kami akan memanggilnya secara asinkron menggunakan Javascript. Kode HTML untuk halaman sekarang akan terlihat seperti ini:

 html = '''<html> <style>html{font-family: Helvetica; display:inline-block; margin: 0px auto; text-align: center;} .button_led {display: inline-block; background-color: #e7bd3b; border: none; border-radius: 4px; color: white; padding: 16px 40px; text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;} </style> <script type="text/javascript" charset="utf-8"> function httpGetAsync(method, callback) { var xmlHttp = new XMLHttpRequest(); xmlHttp.onreadystatechange = function() { if (xmlHttp.readyState == 4 && xmlHttp.status == 200) callback(xmlHttp.responseText); } xmlHttp.open("GET", window.location.href + method, true); xmlHttp.send(null); } function ledOn() { console.log("Led ON..."); httpGetAsync("led/on", function(){ console.log("Done"); }); } function ledOff() { console.log("Led OFF..."); httpGetAsync("led/off", function(){ console.log("Done"); }); } </script> <body> <h2>Hello from the Raspberry Pi!</h2> <p><button class="button button_led" onclick="ledOn();">Led ON</button></p> <p><button class="button button_led" onclick="ledOff();">Led OFF</button></p> </body> </html>''' 

Seperti yang Anda lihat, kami meninggalkan href, dan kami memanggil fungsi ledOn dan ledOff, yang pada gilirannya memanggil metode server yang sesuai secara tidak sinkron (metode asinkron diperlukan agar halaman tidak memblokir sampai respons dari server tiba).

Sekarang tinggal menambahkan pemrosesan permintaan get ke server:

  def do_GET(self): print("GET request, path:", self.path) if self.path == "/": self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(html.encode('utf-8')) elif self.path == "/led/on": self.send_response(200) self.send_header('Content-type', 'text/plain') self.end_headers() rasperrypi_pinout(led_pin, True) self.wfile.write(b"OK") elif self.path == "/led/off": self.send_response(200) self.send_header('Content-type', 'text/plain') self.end_headers() rasperrypi_pinout(led_pin, False) self.wfile.write(b"OK") else: self.send_error(404, "Page Not Found {}".format(self.path)) 

Sekarang, seperti yang Anda lihat, halaman tidak lagi dimuat ketika Anda mencoba menyalakan LED, setiap metode hanya melakukan apa yang seharusnya dilakukan.

Metode 3: Lebih Benar


Semuanya sepertinya sudah bekerja. Tetapi tentu saja, kode di atas dapat (dan harus) ditingkatkan. Faktanya adalah bahwa kita menggunakan permintaan GET untuk mengontrol LED. Ini menghemat ruang kita dalam kode, tetapi secara metodologi ini tidak sepenuhnya benar - permintaan GET dirancang untuk membaca data dari server, mereka dapat di-cache oleh browser, dan umumnya tidak boleh digunakan untuk memodifikasi data. Cara yang benar adalah dengan menggunakan POST (bagi mereka yang tertarik dengan detail, lebih detail di sini ).

Kami akan mengubah panggilan dalam HTML dari posting, tetapi pada saat yang sama, karena kode ini tidak sinkron, kami akan menampilkan status respons server dan tampilan hasilnya. Untuk jaringan lokal ini tidak akan terlihat, tetapi untuk koneksi yang lambat sangat nyaman. Untuk membuatnya lebih menarik, kita akan menggunakan JSON untuk meneruskan parameter.

Versi final terlihat seperti ini:

 html = '''<html> <style>html{font-family: Helvetica; display:inline-block; margin: 0px auto; text-align: center;} .button_led {display: inline-block; background-color: #e7bd3b; border: none; border-radius: 4px; color: white; padding: 16px 40px; text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;} </style> <script type="text/javascript" charset="utf-8"> function httpPostAsync(method, params, callback) { var xmlHttp = new XMLHttpRequest(); xmlHttp.onreadystatechange = function() { if (xmlHttp.readyState == 4 && xmlHttp.status == 200) callback(xmlHttp.responseText); else callback(`Error ${xmlHttp.status}`) } xmlHttp.open("POST", window.location.href + method, true); xmlHttp.setRequestHeader("Content-Type", "application/json"); xmlHttp.send(params); } function ledOn() { document.getElementById("textstatus").textContent = "Making LED on..."; httpPostAsync("led", JSON.stringify({ "on": true }), function(resp) { document.getElementById("textstatus").textContent = `Led ON: ${resp}`; }); } function ledOff() { document.getElementById("textstatus").textContent = "Making LED off..."; httpPostAsync("led", JSON.stringify({ "on": false }), function(resp) { document.getElementById("textstatus").textContent = `Led OFF: ${resp}`; }); } </script> <body> <h2>Hello from the Raspberry Pi!</h2> <p><button class="button button_led" onclick="ledOn();">Led ON</button></p> <p><button class="button button_led" onclick="ledOff();">Led OFF</button></p> <span id="textstatus">Status: Ready</span> </body> </html>''' 

Tambahkan dukungan untuk permintaan GET dan POST ke server:

 import json class ServerHandler(BaseHTTPRequestHandler): def do_GET(self): print("GET request, path:", self.path) if self.path == "/": self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(html.encode('utf-8')) else: self.send_error(404, "Page Not Found {}".format(self.path)) def do_POST(self): content_length = int(self.headers['Content-Length']) body = self.rfile.read(content_length) try: print("POST request, path:", self.path, "body:", body.decode('utf-8')) if self.path == "/led": data_dict = json.loads(body.decode('utf-8')) if 'on' in data_dict: rasperrypi_pinout(led_pin, data_dict['on']) self.send_response(200) self.send_header('Content-type', 'text/plain') self.end_headers() self.wfile.write(b"OK") else: self.send_response(400, 'Bad Request: Method does not exist') self.send_header('Content-Type', 'application/json') self.end_headers() except Exception as err: print("do_POST exception: %s" % str(err)) 

Seperti yang Anda lihat, kami sekarang menggunakan satu fungsi yang dipimpin, di mana parameter "on" dilewatkan menggunakan json, yang menerima True atau False (ketika dipanggil dalam HTML, string json dari bentuk {"on": true} ditransmisikan, masing-masing). Perlu juga diperhatikan untuk mencoba-menangkap - ini memblokir server agar tidak jatuh, misalnya, jika seseorang mengirim string dengan json yang tidak valid ke server.

Jika semuanya dilakukan dengan benar, kami mendapatkan server dengan umpan balik, yang seharusnya terlihat seperti ini:



Umpan balik, dalam kasus kami, pesan "OK", memungkinkan Anda untuk melihat konfirmasi dari server bahwa kode sebenarnya telah diproses.

Bisakah server ini diperbaiki? Anda dapat, misalnya, masuk akal untuk mengganti penggunaan fungsi cetak dengan menggunakan pencatatan, ini lebih tepat, dan memungkinkan Anda untuk menampilkan log server tidak hanya di layar, tetapi juga jika Anda ingin menulisnya ke file dengan rotasi otomatis. Mereka yang berharap dapat melakukan ini sendiri.

Kesimpulan


Jika semuanya dilakukan dengan benar, kami akan mendapatkan mini-server yang memungkinkan Anda untuk mengontrol LED atau perangkat lain melalui browser dari perangkat jaringan apa pun.

Penting: Tindakan Pencegahan Keselamatan

Sekali lagi saya perhatikan bahwa tidak ada perlindungan atau otentikasi di sini, jadi Anda tidak boleh "memposting" halaman seperti itu di Internet jika Anda berencana untuk mengelola beberapa beban yang lebih atau kurang bertanggung jawab. Meskipun kasus-kasus serangan pada server semacam itu tidak saya ketahui, masih ada gunanya memberikan siapa pun yang ingin bisa membuka pintu garasi dari jarak jauh atau menyalakan pemanas kilowatt. Jika Anda ingin mengontrol dari jarak jauh melalui halaman seperti itu, ada baiknya menyiapkan VPN atau yang serupa.

Sebagai kesimpulan, saya ulangi bahwa materi ini dirancang untuk pemula, dan saya harap ini lebih atau kurang bermanfaat. Jelas bahwa tidak semua orang di Khabr puas dengan ketersediaan artikel "untuk boneka", jadi apakah bagian selanjutnya akan tergantung pada nilai akhir. Jika ada, maka akan mempertimbangkan kerangka kerja Flask dan WSGI, serta metode otentikasi dasar.

Semua percobaan berhasil.

Source: https://habr.com/ru/post/id472464/


All Articles