ESP32和python将电脑屏幕投射到小屏幕

上位机和下位机的代码都会开源

思路简析

使用python的socket模块和ardunio的wifi库即可建立服务器和客户端打通电脑和ESP32的连接,传输速度相当可观。
打通连接之后再确定一个简单的协议即可,在这里确定了一个非常简单的协议,只需要两步:

1.建立连接后,下位机会立刻发送字符’L’给上位机,上位机收到后立刻发送这一帧的长度给下位机
2.下位机收到后再立刻发送字符‘Y’给上位机,上位机收到后立刻将这一帧图片发送给下位机,下位机解码输出后再发送’L’给上位机,如此循环。

上下位机的代码

上位机代码

需要的模块中除了PIL库需要额外下载外,其他库都是自带的。
PIL 库的下载也很简单,在cmd中输入下面这一句命令即可

pip install PIL
from tkinter import *
from socket import *
from tkinter import filedialog
import threading
from time import strftime
import os
from PIL import ImageGrab #截图库
#--------------------------------------------#
class myapp(Tk):
    def __init__(self):
        super().__init__()
        self.title("完形填空")
        self.geometry("240x340")
        #b1=Button(self,text="投屏",command=self.toupin)
        #b1.place(x=80,y=0,width=80,height=40)               这个按钮用作测试用,实际程序中不需要,所以注释掉
        b2=Button(self,text="连接",command=self.lianjie)
        b2.place(x=0,y=0,width=120,height=40)
        b3=Button(self,text="关闭连接",command=self.guanbi)
        b3.place(x=120,y=0,width=120,height=40)
        self.text=Text(self)
        self.text.place(x=0,y=40,width=240,height=60)            #各种GUI控件,布局

        self.s=socket(AF_INET,SOCK_STREAM)
        self.add=('',6666)
        self.s.bind(self.add)
        self.s.listen(5)                                        #服务器配置和开启监听
        self.xian=1
        
    def toupin(self):

        while self.xian:
            sou=self.tap.recv(1024)
            if sou.decode()=='L':
                pic = ImageGrab.grab((100,200,340,440))          #在这里设置截屏区域,分别是左上角坐标和右下角坐标
                pic.save('1.jpg')
                self.text.insert(END,strftime("%H:%M:%S")+': '+sou.decode()+'\n')

                self.l=int(os.path.getsize('1.jpg'))

                self.tap.send(int(os.path.getsize('1.jpg')).to_bytes(2,byteorder='little',signed=True))
            elif sou.decode()=='Y':
                self.text.insert(END,strftime("%H:%M:%S")+': '+sou.decode()+'\n')
                self.file=open('1.jpg','rb+')
                while self.l>512:
                    self.cc=self.file.read(512)
                    self.tap.send(self.cc)
                    self.l-=512
                self.cc=self.file.read(self.l)
                self.tap.send(self.cc)
                self.file.close()
    #收发过程可以理解为:1.下位机先给上位机发送'L',上位机收到后给下位机发送这一帧的长度
    #                  2.下位机收到长度后给上位机发送'Y',上位机收到后发送图片信息
    #                  3.下位机收到后解码输出图片后再继续发送'L',代表下一帧开始
    #如此循环
    #PS:上位机这里用了一个文件作中转站,作者实在是不知道如何将RGB通道改成jpg格式
    #    有懂的朋友可以评论区告诉我一下


    def lianjie(self):
        self.tap,self.addr=self.s.accept()
        print(self.addr)
        self.xian=1
        self.t1=threading.Thread(target=self.toupin)
        self.t1.start()
    def guanbi(self):
        self.xian=0
    

if __name__=='__main__':
    app1=myapp()
    app1.mainloop()
下位机代码

下位机代码是C++语言,环境是Ardunio环境

//------------需要的库------------//
#include <TFT_eSPI.h>
#include <SPI.h>
#include <WiFi.h>
#include <TJpg_Decoder.h>
TFT_eSPI tft = TFT_eSPI(); 
//------在这里定义你的各种参数-------//
char* ssid     = "WiFi-name"; //填写你的wifi名字
char* passwrd = ""; //填写你的wifi密码
char* service_ip="132.178.100.4";//上位机IP地址
int httpPort = 6666; //设置上位机端口

//----下面的参数用于loadingcartoon函数-----//
char tai=0,i;
char load[8][2]={{100,120},{110,110,},{120,100},{130,110},{140,120},{130,130},{120,140},{110,130}};

//-------以下参数用于收发图片--------------//
WiFiClient client; //初始化一个客户端对象
TaskHandle_t loading= NULL;
uint8_t buff[7000] PROGMEM= {0};//每一帧的临时缓存
uint8_t img_buff[40000] PROGMEM= {0};//用于存储tcp传过来的图片,注意图片大小不要超出内存,分辨率高的屏幕可以扩容
uint16_t size_count=0;//计算一帧的字节大小
uint16_t read_count,s_time,e_time,total_count=0;
uint8_t changdu[2];


//------------------------------------//
bool tft_output(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t* bitmap)//jpg解码回调函数
{
  if ( y >= tft.height() ) return 0;
  tft.pushImage(x, y, w, h, bitmap);
  // Return 1 to decode next block
  return 1;
}


//------------------------------------------------------------------//

void loadingcartoon(void *pvParameters)  // This is a task.
{
  //(void) pvParameters;
  for (;;) // A Task shall never return or exit.
  {
  switch(tai)
  {
  case 0:
  for(i=0;i<=7;i++)
  {
    tft.fillCircle(load[i][0],load[i][1],2,TFT_RED);
    //delay(100);
    vTaskDelay(100);
  }
  tai=1;
  break;
  case 1:
  for(i=0;i<=7;i++)
  {
    tft.fillCircle(load[i][0],load[i][1],4,TFT_RED);
    //delay(100);
    vTaskDelay(100);
  }
  tai=2;
  break;
  case 2:
  for(i=0;i<=7;i++)
  {
    tft.fillCircle(load[i][0],load[i][1],4,TFT_WHITE);
    //delay(100);
    vTaskDelay(100);
  }
  tai=0;
  break;
  
  }
  }
}
//----------------------------------------------------//

void setup(){
  Serial.begin(115200);
  tft.begin();
  tft.setRotation(0);//竖屏240x240
  tft.fillScreen(TFT_WHITE);//白屏
  tft.setTextColor(TFT_BLACK,TFT_WHITE);
  tft.fillRect(20, 100,200,20, TFT_WHITE);
  tft.drawString("conecting..",30,150,4);
  xTaskCreate(loadingcartoon,"loading",1024,NULL,1,&loading);   //开启动画线程
  WiFi.begin(ssid, pasword); //连接wifi
  delay(1000); //等待1秒
  while (WiFi.status() != WL_CONNECTED) {
    for(byte n=0;n<10;n++){ //每500毫秒检测一次状态
      //loading(50);
      delay(500);
    }
  }

  if (WiFi.status() == WL_CONNECTED) //判断如果wifi连接成功
  { 
    client.connect(service_ip,httpPort); //连接到上位机
    Serial.println("wifi is connected!");
    Serial.print("SSID: ");
    Serial.println(WiFi.SSID());
    IPAddress ip = WiFi.localIP();
    Serial.print("IP Address: ");
    Serial.println(ip);
  }
  else Serial.printf("connect feid");
  vTaskDelete(loading);                                  //连接成功后删除线程
  tft.drawString("conected....",30,150,4);
  delay(2000);
  tft.fillScreen(TFT_BLACK);
  TJpgDec.setJpgScale(1);
  TJpgDec.setSwapBytes(true);
  TJpgDec.setCallback(tft_output);//解码成功回调函数
  
  
}


//-------------------------------------------------//
void loop(){
  
  s_time=millis();
  client.write("L");
  delay(1);
  
  while(1)
  {
  if(client.available())
    {
      read_count=client.read(changdu,1024);
      size_count=changdu[0]+changdu[1]*0x100;
      Serial.println(size_count);
      read_count=0;
      client.write("Y");
      while(total_count<size_count)
      {
         if(client.available())
        {
          read_count=client.read(buff,7000);//向缓冲区读取数据
          memcpy(&img_buff[total_count],buff,read_count);//将读取的buff字节地址复制给img_buff数组 
          total_count+=read_count;
       
        }
      }
      Serial.println(total_count);
      TJpgDec.drawJpg(0,0,img_buff, sizeof(img_buff));//将jpg图片解码为bmp
      memset(&img_buff,0,sizeof(img_buff));//清空buff
      total_count=0;
    }
   else continue;
   break;
  }
  e_time=millis();
  Serial.printf("当前处理能力:");
  Serial.print(1000/(e_time-s_time),2);
  Serial.printf("Fps\n");
  
}

新下载的TJpg_Decoder可能会编译错误,而且他还需要一个较为冷门的库,在这里献上我调试好的库,只需要将之复制粘贴到Ardunio的library中即可。

链接: 点击这里下载库文件 链接:https://pan.baidu.com/s/1VmfNl7qXzZvYhMSolFg1UA?pwd=w7e2
提取码:w7e2

其实官网下载的TJpg_Decoder库中也就只有一个错误而已,在编译的时候就会发现其使用的littlefs改成与littlefs库文件中的头文件一致即可。

总结

tkinter程序界面中留有240x240像素的空白,原本是给截图预留的展示区域,但是tkinter程序刷新图片会频闪,谁能解决评论区告诉一下。

在连接电脑的热点时,刷新能够达到15Fps左右。