// SPDX-License-Identifier: GPL-3.0-or-later // references: // https://api.libssh.org/stable/libssh_tutorial.html // https://api.libssh.org/stable/libssh_tutor_guided_tour.html // https://plplot.sourceforge.net/documentation.php // https://plplot.sourceforge.net/examples.php // plplot example 0 and ext-cairo-test.c // https://docs.gtk.org/gtk4/class.DrawingArea.html // https://www.gtk.org/docs/getting-started/hello-world/ // https://gitlab.gnome.org/GNOME/gtk/-/blob/main/demos/print-editor/print-editor.c // https://gitlab.gnome.org/GNOME/gtk/-/blob/main/demos/gtk-demo/main.c // libssh, license LGPL-2.1-or-later #include #include #include #include // verify_knownhost() #include #include #include // open() // plplot, license LGPL-2.0-or-later, it also have codes licensed under other // licenses but I don't think my program used those codes #include #include #include //#include #include #include #include // license LGPL-2.1-or-later // Using a linked list to hold all the voltages and temperatures data #include "list.h" #include //uint32_t // use double for PLFLT #define PL_DOUBLE // max wire unsigned number is 65535 // max wire signed number maybe is -32768 to 32767 // voltage max format 7 bytes ,6.5535 // temp max format 4 bytes ,255 // timestamp max 10 bytes 1745742182 // 1 timestamp, 96 voltage, 48 temp, 1 \n // 10+96*7+96*4+1=1067 bytes #define ENTRY_SIZE 1067 // 16 KiB == 16384 bytes // https://api.libssh.org/stable/libssh_tutor_sftp.html #define MAX_XFER_BUF_SIZE (16384/ENTRY_SIZE*ENTRY_SIZE) const char *checkbutton_names[]={ "0x630_BMS_M1_Cell_1_Voltage", "0x630_BMS_M1_Cell_2_Voltage", "0x630_BMS_M1_Cell_3_Voltage", "0x630_BMS_M1_Cell_4_Voltage", "0x631_BMS_M1_Cell_5_Voltage", "0x631_BMS_M1_Cell_6_Voltage", "0x631_BMS_M1_Cell_7_Voltage", "0x631_BMS_M1_Cell_8_Voltage", "0x632_BMS_M1_Cell_9_Voltage", "0x632_BMS_M1_Cell_10_Voltage", "0x632_BMS_M1_Cell_11_Voltage", "0x632_BMS_M1_Cell_12_Voltage", "0x633_BMS_M2_Cell_1_Voltage", "0x633_BMS_M2_Cell_2_Voltage", "0x633_BMS_M2_Cell_3_Voltage", "0x633_BMS_M2_Cell_4_Voltage", "0x634_BMS_M2_Cell_5_Voltage", "0x634_BMS_M2_Cell_6_Voltage", "0x634_BMS_M2_Cell_7_Voltage", "0x634_BMS_M2_Cell_8_Voltage", "0x635_BMS_M2_Cell_9_Voltage", "0x635_BMS_M2_Cell_10_Voltage", "0x635_BMS_M2_Cell_11_Voltage", "0x635_BMS_M2_Cell_12_Voltage", "0x636_BMS_M3_Cell_1_Voltage", "0x636_BMS_M3_Cell_2_Voltage", "0x636_BMS_M3_Cell_3_Voltage", "0x636_BMS_M3_Cell_4_Voltage", "0x637_BMS_M3_Cell_5_Voltage", "0x637_BMS_M3_Cell_6_Voltage", "0x637_BMS_M3_Cell_7_Voltage", "0x637_BMS_M3_Cell_8_Voltage", "0x638_BMS_M3_Cell_9_Voltage", "0x638_BMS_M3_Cell_10_Voltage", "0x638_BMS_M3_Cell_11_Voltage", "0x638_BMS_M3_Cell_12_Voltage", "0x639_BMS_M4_Cell_1_Voltage", "0x639_BMS_M4_Cell_2_Voltage", "0x639_BMS_M4_Cell_3_Voltage", "0x639_BMS_M4_Cell_4_Voltage", "0x63a_BMS_M4_Cell_5_Voltage", "0x63a_BMS_M4_Cell_6_Voltage", "0x63a_BMS_M4_Cell_7_Voltage", "0x63a_BMS_M4_Cell_8_Voltage", "0x63b_BMS_M4_Cell_9_Voltage", "0x63b_BMS_M4_Cell_10_Voltage", "0x63b_BMS_M4_Cell_11_Voltage", "0x63b_BMS_M4_Cell_12_Voltage", "0x63c_BMS_M5_Cell_1_Voltage", "0x63c_BMS_M5_Cell_2_Voltage", "0x63c_BMS_M5_Cell_3_Voltage", "0x63c_BMS_M5_Cell_4_Voltage", "0x63d_BMS_M5_Cell_5_Voltage", "0x63d_BMS_M5_Cell_6_Voltage", "0x63d_BMS_M5_Cell_7_Voltage", "0x63d_BMS_M5_Cell_8_Voltage", "0x63e_BMS_M5_Cell_9_Voltage", "0x63e_BMS_M5_Cell_10_Voltage", "0x63e_BMS_M5_Cell_11_Voltage", "0x63e_BMS_M5_Cell_12_Voltage", "0x63f_BMS_M6_Cell_1_Voltage", "0x63f_BMS_M6_Cell_2_Voltage", "0x63f_BMS_M6_Cell_3_Voltage", "0x63f_BMS_M6_Cell_4_Voltage", "0x640_BMS_M6_Cell_5_Voltage", "0x640_BMS_M6_Cell_6_Voltage", "0x640_BMS_M6_Cell_7_Voltage", "0x640_BMS_M6_Cell_8_Voltage", "0x641_BMS_M6_Cell_9_Voltage", "0x641_BMS_M6_Cell_10_Voltage", "0x641_BMS_M6_Cell_11_Voltage", "0x641_BMS_M6_Cell_12_Voltage", "0x642_BMS_M7_Cell_1_Voltage", "0x642_BMS_M7_Cell_2_Voltage", "0x642_BMS_M7_Cell_3_Voltage", "0x642_BMS_M7_Cell_4_Voltage", "0x643_BMS_M7_Cell_5_Voltage", "0x643_BMS_M7_Cell_6_Voltage", "0x643_BMS_M7_Cell_7_Voltage", "0x643_BMS_M7_Cell_8_Voltage", "0x644_BMS_M7_Cell_9_Voltage", "0x644_BMS_M7_Cell_10_Voltage", "0x644_BMS_M7_Cell_11_Voltage", "0x644_BMS_M7_Cell_12_Voltage", "0x645_BMS_M8_Cell_1_Voltage", "0x645_BMS_M8_Cell_2_Voltage", "0x645_BMS_M8_Cell_3_Voltage", "0x645_BMS_M8_Cell_4_Voltage", "0x646_BMS_M8_Cell_5_Voltage", "0x646_BMS_M8_Cell_6_Voltage", "0x646_BMS_M8_Cell_7_Voltage", "0x646_BMS_M8_Cell_8_Voltage", "0x647_BMS_M8_Cell_9_Voltage", "0x647_BMS_M8_Cell_10_Voltage", "0x647_BMS_M8_Cell_11_Voltage", "0x647_BMS_M8_Cell_12_Voltage", "0x680_BMS_M1_Cell_1_Thermistor", "0x680_BMS_M1_Cell_2_Thermistor", "0x680_BMS_M1_Cell_3_Thermistor", "0x680_BMS_M1_Cell_4_Thermistor", "0x680_BMS_M1_Cell_5_Thermistor", "0x680_BMS_M1_Cell_6_Thermistor", "0x680_BMS_M1_Cell_7_Thermistor", "0x680_BMS_M1_Cell_8_Thermistor", "0x681_BMS_M1_Cell_9_Thermistor", "0x681_BMS_M1_Cell_10_Thermistor", "0x681_BMS_M1_Cell_11_Thermistor", "0x681_BMS_M1_Cell_12_Thermistor", "0x682_BMS_M2_Cell_1_Thermistor", "0x682_BMS_M2_Cell_2_Thermistor", "0x682_BMS_M2_Cell_3_Thermistor", "0x682_BMS_M2_Cell_4_Thermistor", "0x682_BMS_M2_Cell_5_Thermistor", "0x682_BMS_M2_Cell_6_Thermistor", "0x682_BMS_M2_Cell_7_Thermistor", "0x682_BMS_M2_Cell_8_Thermistor", "0x683_BMS_M2_Cell_9_Thermistor", "0x683_BMS_M2_Cell_10_Thermistor", "0x683_BMS_M2_Cell_11_Thermistor", "0x683_BMS_M2_Cell_12_Thermistor", "0x684_BMS_M3_Cell_1_Thermistor", "0x684_BMS_M3_Cell_2_Thermistor", "0x684_BMS_M3_Cell_3_Thermistor", "0x684_BMS_M3_Cell_4_Thermistor", "0x684_BMS_M3_Cell_5_Thermistor", "0x684_BMS_M3_Cell_6_Thermistor", "0x684_BMS_M3_Cell_7_Thermistor", "0x684_BMS_M3_Cell_8_Thermistor", "0x685_BMS_M3_Cell_9_Thermistor", "0x685_BMS_M3_Cell_10_Thermistor", "0x685_BMS_M3_Cell_11_Thermistor", "0x685_BMS_M3_Cell_12_Thermistor", "0x686_BMS_M4_Cell_1_Thermistor", "0x686_BMS_M4_Cell_2_Thermistor", "0x686_BMS_M4_Cell_3_Thermistor", "0x686_BMS_M4_Cell_4_Thermistor", "0x686_BMS_M4_Cell_5_Thermistor", "0x686_BMS_M4_Cell_6_Thermistor", "0x686_BMS_M4_Cell_7_Thermistor", "0x686_BMS_M4_Cell_8_Thermistor", "0x687_BMS_M4_Cell_9_Thermistor", "0x687_BMS_M4_Cell_10_Thermistor", "0x687_BMS_M4_Cell_11_Thermistor", "0x687_BMS_M4_Cell_12_Thermistor", "0x688_BMS_M5_Cell_1_Thermistor", "0x688_BMS_M5_Cell_2_Thermistor", "0x688_BMS_M5_Cell_3_Thermistor", "0x688_BMS_M5_Cell_4_Thermistor", "0x688_BMS_M5_Cell_5_Thermistor", "0x688_BMS_M5_Cell_6_Thermistor", "0x688_BMS_M5_Cell_7_Thermistor", "0x688_BMS_M5_Cell_8_Thermistor", "0x689_BMS_M5_Cell_9_Thermistor", "0x689_BMS_M5_Cell_10_Thermistor", "0x689_BMS_M5_Cell_11_Thermistor", "0x689_BMS_M5_Cell_12_Thermistor", "0x68a_BMS_M6_Cell_1_Thermistor", "0x68a_BMS_M6_Cell_2_Thermistor", "0x68a_BMS_M6_Cell_3_Thermistor", "0x68a_BMS_M6_Cell_4_Thermistor", "0x68a_BMS_M6_Cell_5_Thermistor", "0x68a_BMS_M6_Cell_6_Thermistor", "0x68a_BMS_M6_Cell_7_Thermistor", "0x68a_BMS_M6_Cell_8_Thermistor", "0x68b_BMS_M6_Cell_9_Thermistor", "0x68b_BMS_M6_Cell_10_Thermistor", "0x68b_BMS_M6_Cell_11_Thermistor", "0x68b_BMS_M6_Cell_12_Thermistor", "0x68c_BMS_M7_Cell_1_Thermistor", "0x68c_BMS_M7_Cell_2_Thermistor", "0x68c_BMS_M7_Cell_3_Thermistor", "0x68c_BMS_M7_Cell_4_Thermistor", "0x68c_BMS_M7_Cell_5_Thermistor", "0x68c_BMS_M7_Cell_6_Thermistor", "0x68c_BMS_M7_Cell_7_Thermistor", "0x68c_BMS_M7_Cell_8_Thermistor", "0x68d_BMS_M7_Cell_9_Thermistor", "0x68d_BMS_M7_Cell_10_Thermistor", "0x68d_BMS_M7_Cell_11_Thermistor", "0x68d_BMS_M7_Cell_12_Thermistor", "0x68e_BMS_M8_Cell_1_Thermistor", "0x68e_BMS_M8_Cell_2_Thermistor", "0x68e_BMS_M8_Cell_3_Thermistor", "0x68e_BMS_M8_Cell_4_Thermistor", "0x68e_BMS_M8_Cell_5_Thermistor", "0x68e_BMS_M8_Cell_6_Thermistor", "0x68e_BMS_M8_Cell_7_Thermistor", "0x68e_BMS_M8_Cell_8_Thermistor", "0x68f_BMS_M8_Cell_9_Thermistor", "0x68f_BMS_M8_Cell_10_Thermistor", "0x68f_BMS_M8_Cell_11_Thermistor", "0x68f_BMS_M8_Cell_12_Thermistor" }; // I passed this structure's pointer to many functions for them to use, which I think is kinda like the gtk style typedef struct { ssh_session session; sftp_session sftp; List cans; GtkWidget *area; GtkWidget *checkbutton[VOLTLEN+TEMPLEN]; GtkWidget *volt_label; GtkWidget *temp_label; GtkWidget *live_toggle; GtkWidget *from_entry; GtkWidget *to_entry; GtkWidget *unix_time_toggle; GtkWidget *last_seconds_toggle; // .csv file already read offset uint32_t offset; uint32_t from_time; uint32_t to_time; bool from_time_entered; //bool to_time_entered; // local file name char *filename; // only read from local .csv file or not bool local; // just start up the program bool just_start; }DATA; // Read data from local. Or read data from local and remote and write to local. static gboolean read_data(gpointer user_data) { //printf("read_data begin\n"); DATA *data=user_data; bool new_entry=false; // Read data from local if specify read from local on command line, or the program just started. if(data->local || data->just_start) { if(data->just_start) data->just_start=false; //printf("read_data check local pass\n"); FILE *fp; if((fp=fopen(data->filename,"r"))!=NULL) { //printf("offset before fseek %d\n",data->offset); fseek(fp, data->offset, SEEK_SET); //printf("before read_data local for loop\n"); for(;;) { //printf("start read_data local for loop\n"); Item temp; int ct=fscanf(fp,"%u,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu,%hhu",&temp.t,&temp.volt[0],&temp.volt[1],&temp.volt[2],&temp.volt[3],&temp.volt[4],&temp.volt[5],&temp.volt[6],&temp.volt[7],&temp.volt[8],&temp.volt[9],&temp.volt[10],&temp.volt[11],&temp.volt[12],&temp.volt[13],&temp.volt[14],&temp.volt[15],&temp.volt[16],&temp.volt[17],&temp.volt[18],&temp.volt[19],&temp.volt[20],&temp.volt[21],&temp.volt[22],&temp.volt[23],&temp.volt[24],&temp.volt[25],&temp.volt[26],&temp.volt[27],&temp.volt[28],&temp.volt[29],&temp.volt[30],&temp.volt[31],&temp.volt[32],&temp.volt[33],&temp.volt[34],&temp.volt[35],&temp.volt[36],&temp.volt[37],&temp.volt[38],&temp.volt[39],&temp.volt[40],&temp.volt[41],&temp.volt[42],&temp.volt[43],&temp.volt[44],&temp.volt[45],&temp.volt[46],&temp.volt[47],&temp.volt[48],&temp.volt[49],&temp.volt[50],&temp.volt[51],&temp.volt[52],&temp.volt[53],&temp.volt[54],&temp.volt[55],&temp.volt[56],&temp.volt[57],&temp.volt[58],&temp.volt[59],&temp.volt[60],&temp.volt[61],&temp.volt[62],&temp.volt[63],&temp.volt[64],&temp.volt[65],&temp.volt[66],&temp.volt[67],&temp.volt[68],&temp.volt[69],&temp.volt[70],&temp.volt[71],&temp.volt[72],&temp.volt[73],&temp.volt[74],&temp.volt[75],&temp.volt[76],&temp.volt[77],&temp.volt[78],&temp.volt[79],&temp.volt[80],&temp.volt[81],&temp.volt[82],&temp.volt[83],&temp.volt[84],&temp.volt[85],&temp.volt[86],&temp.volt[87],&temp.volt[88],&temp.volt[89],&temp.volt[90],&temp.volt[91],&temp.volt[92],&temp.volt[93],&temp.volt[94],&temp.volt[95],&temp.temp[0],&temp.temp[1],&temp.temp[2],&temp.temp[3],&temp.temp[4],&temp.temp[5],&temp.temp[6],&temp.temp[7],&temp.temp[8],&temp.temp[9],&temp.temp[10],&temp.temp[11],&temp.temp[12],&temp.temp[13],&temp.temp[14],&temp.temp[15],&temp.temp[16],&temp.temp[17],&temp.temp[18],&temp.temp[19],&temp.temp[20],&temp.temp[21],&temp.temp[22],&temp.temp[23],&temp.temp[24],&temp.temp[25],&temp.temp[26],&temp.temp[27],&temp.temp[28],&temp.temp[29],&temp.temp[30],&temp.temp[31],&temp.temp[32],&temp.temp[33],&temp.temp[34],&temp.temp[35],&temp.temp[36],&temp.temp[37],&temp.temp[38],&temp.temp[39],&temp.temp[40],&temp.temp[41],&temp.temp[42],&temp.temp[43],&temp.temp[44],&temp.temp[45],&temp.temp[46],&temp.temp[47],&temp.temp[48],&temp.temp[49],&temp.temp[50],&temp.temp[51],&temp.temp[52],&temp.temp[53],&temp.temp[54],&temp.temp[55],&temp.temp[56],&temp.temp[57],&temp.temp[58],&temp.temp[59],&temp.temp[60],&temp.temp[61],&temp.temp[62],&temp.temp[63],&temp.temp[64],&temp.temp[65],&temp.temp[66],&temp.temp[67],&temp.temp[68],&temp.temp[69],&temp.temp[70],&temp.temp[71],&temp.temp[72],&temp.temp[73],&temp.temp[74],&temp.temp[75],&temp.temp[76],&temp.temp[77],&temp.temp[78],&temp.temp[79],&temp.temp[80],&temp.temp[81],&temp.temp[82],&temp.temp[83],&temp.temp[84],&temp.temp[85],&temp.temp[86],&temp.temp[87],&temp.temp[88],&temp.temp[89],&temp.temp[90],&temp.temp[91],&temp.temp[92],&temp.temp[93],&temp.temp[94],&temp.temp[95]); if(ct == EOF) break; else if(ct<(VOLTLEN+TEMPLEN+1)) { fprintf(stderr,"Read less items (%d) than expected from local file\n",ct); //exit(1); return G_SOURCE_REMOVE; } if(AddItem(temp,&data->cans)==false) { fprintf(stderr,"Problem allocating memory\n"); //exit(1); return G_SOURCE_REMOVE; } if(!new_entry) new_entry=true; //printf("end read_data local for loop\n"); } //printf("after read_data local for loop\n"); //printf("offset before ftell %d\n",data->offset); data->offset=ftell(fp); //printf("offset after ftell %d\n",data->offset); fclose(fp); } } // Read data from remote else { int access_type; sftp_file file; char buffer[MAX_XFER_BUF_SIZE]; int nbytes, rc; access_type = O_RDONLY; //printf("before sftp_open\n"); // No need `sftp_expand_path(data->sftp,"~/.local/share/mycan.csv")` because the current dir seems is home dir. // Also old libssh seems does not have sftp_expand_path file = sftp_open(data->sftp, ".local/share/mycan.csv", access_type, 0); //printf("after sftp_open\n"); if (file == NULL) { fprintf(stderr, "Can't open file for reading: %s\n", ssh_get_error(data->session)); //return SSH_ERROR; return G_SOURCE_REMOVE; } //printf("before sftp_seed\n"); if(data->offset != 0) sftp_seek(file,data->offset); //printf("after sftp_seed\n"); for(;;) { //printf("before sftp_read\n"); nbytes = sftp_read(file, buffer, sizeof(buffer)); //printf("after sftp_read\n"); //printf("nbytes: %d\n",nbytes); //printf("%d\n",MAX_XFER_BUF_SIZE); if (nbytes == 0) { break; // EOF } else if (nbytes < 0) { fprintf(stderr, "Error while reading file: %s\n", ssh_get_error(data->session)); sftp_close(file); //return SSH_ERROR; return G_SOURCE_REMOVE; } else if ((nbytes%ENTRY_SIZE) != 0) { fprintf(stderr, "sftp read nbytes not a multiple of %d\n",ENTRY_SIZE); sftp_close(file); //return SSH_ERROR; return G_SOURCE_REMOVE; } // TODO: check strtok return NULL or not //printf("before strtok\n"); for(int i=0;i<(nbytes/ENTRY_SIZE);i++) { Item temp; FILE *fp; // TODO: pass endptr and check it and other things to be safe, see https://stackoverflow.com/questions/34206446 if(i) temp.t=strtoul(strtok(NULL,","),NULL,10); else temp.t=strtoul(strtok(buffer,","),NULL,10); for(int j=0;jcans)==false) { fprintf(stderr,"Problem allocating memory\n"); return G_SOURCE_REMOVE; } if(!new_entry) new_entry=true; //printf("after AddItem\n"); if((fp=fopen(data->filename,"a"))==NULL) { fprintf(stderr,"Can't open file \"%s\".\n",data->filename); //exit(1); return G_SOURCE_REMOVE; } fprintf(fp,"%u",temp.t); for(int j=0;joffset+=nbytes; } rc = sftp_close(file); if (rc != SSH_OK) { fprintf(stderr, "Can't close the read file: %s\n", ssh_get_error(data->session)); //return rc; return G_SOURCE_REMOVE; } } //printf("after read_data check local if else\n"); // Get and calculate min, max, and average of voltages and temperatures. And update corresponding labels. if(new_entry) { double sum, avg, vmin, vmax; uint8_t tmin, tmax; // min/avg/max temperature: 255/254/255 C // min/avg/max voltage: 6.5535/6.5535/6.5535 V // max 43 char + 1 \0 char str[44]; sum=avg=0; vmin=vmax=data->cans.end->item.volt[0]; tmin=tmax=data->cans.end->item.temp[0]; for(int i=0;icans.end->item.volt[i]); sum+=data->cans.end->item.volt[i]; if(i>0) { if(vmin > data->cans.end->item.volt[i]) vmin=data->cans.end->item.volt[i]; if(vmax < data->cans.end->item.volt[i]) vmax=data->cans.end->item.volt[i]; } } avg=sum/VOLTLEN; sprintf(str,"min/avg/max voltage: %6.4f/%6.4f/%6.4f V",vmin,avg,vmax); gtk_label_set_text(GTK_LABEL(data->volt_label),str); sum=0; for(int i=0;icans.end->item.temp[i]); sum+=data->cans.end->item.temp[i]; if(i>0) { if(tmin > data->cans.end->item.temp[i]) tmin=data->cans.end->item.temp[i]; if(tmax < data->cans.end->item.temp[i]) tmax=data->cans.end->item.temp[i]; } } avg=sum/TEMPLEN; sprintf(str,"min/avg/max temperature: %3hhu/%.0f/%3hhu C",tmin,avg,tmax); gtk_label_set_text(GTK_LABEL(data->temp_label),str); new_entry=false; // Update drawing area gtk_widget_queue_draw(data->area); } //printf("after check new_entry if\n"); //return SSH_OK; // must return G_SOURCE_CONTINUE to keep polling // return G_SOURCE_REMOVE to stop return G_SOURCE_CONTINUE; } // https://api.libssh.org/stable/libssh_tutor_guided_tour.html int verify_knownhost(ssh_session session) { enum ssh_known_hosts_e state; unsigned char *hash = NULL; ssh_key srv_pubkey = NULL; size_t hlen; char buf[10]; char *hexa; char *p; int cmp; int rc; rc = ssh_get_server_publickey(session, &srv_pubkey); if (rc < 0) { return -1; } rc = ssh_get_publickey_hash(srv_pubkey, SSH_PUBLICKEY_HASH_SHA1, &hash, &hlen); ssh_key_free(srv_pubkey); if (rc < 0) { return -1; } state = ssh_session_is_known_server(session); switch (state) { case SSH_KNOWN_HOSTS_OK: /* OK */ break; case SSH_KNOWN_HOSTS_CHANGED: fprintf(stderr, "Host key for server changed: it is now:\n"); ssh_print_hexa("Public key hash", hash, hlen); fprintf(stderr, "For security reasons, connection will be stopped\n"); ssh_clean_pubkey_hash(&hash); return -1; case SSH_KNOWN_HOSTS_OTHER: fprintf(stderr, "The host key for this server was not found but an other" "type of key exists.\n"); fprintf(stderr, "An attacker might change the default server key to" "confuse your client into thinking the key does not exist\n"); ssh_clean_pubkey_hash(&hash); return -1; case SSH_KNOWN_HOSTS_NOT_FOUND: fprintf(stderr, "Could not find known host file.\n"); fprintf(stderr, "If you accept the host key here, the file will be" "automatically created.\n"); /* FALL THROUGH to SSH_SERVER_NOT_KNOWN behavior */ case SSH_KNOWN_HOSTS_UNKNOWN: hexa = ssh_get_hexa(hash, hlen); fprintf(stderr,"The server is unknown. Do you trust the host key?\n"); fprintf(stderr, "Public key hash: %s\n", hexa); ssh_string_free_char(hexa); ssh_clean_pubkey_hash(&hash); p = fgets(buf, sizeof(buf), stdin); if (p == NULL) { return -1; } cmp = strncasecmp(buf, "yes", 3); if (cmp != 0) { return -1; } rc = ssh_session_update_known_hosts(session); if (rc < 0) { fprintf(stderr, "Error %s\n", strerror(errno)); return -1; } break; case SSH_KNOWN_HOSTS_ERROR: fprintf(stderr, "Error %s", ssh_get_error(session)); ssh_clean_pubkey_hash(&hash); return -1; } ssh_clean_pubkey_hash(&hash); return 0; } // https://plplot.sourceforge.net/documentation.php // https://plplot.sourceforge.net/examples.php // plplot example 0 and ext-cairo-test.c // Draw plots on gtk drawing area static void draw_function (GtkDrawingArea *area, cairo_t *cr, int width, int height, gpointer user_data) { DATA *data=user_data; if(ListIsEmpty(&data->cans)) return; //printf("draw begin\n"); //PLFLT xmin=data->cans.head->item.t; //PLFLT xmax=data->cans.end->item.t; PLFLT xmin,xmax; PLFLT ymin, ymax; int plot_counts=0; gboolean active_checkbutton[VOLTLEN+TEMPLEN]; if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(data->live_toggle))) xmax=data->cans.end->item.t; else xmax=data->to_time; if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(data->last_seconds_toggle))) xmin=data->cans.end->item.t-50; else if(data->from_time_entered) xmin=data->from_time; else xmin=data->cans.head->item.t; for(int i=0;i<(VOLTLEN+TEMPLEN);i++) if((active_checkbutton[i]=gtk_check_button_get_active(GTK_CHECK_BUTTON(data->checkbutton[i])))) plot_counts++; // Plplot set to use extcairo device plsdev( "extcairo" ); { char str[12]; sprintf(str,"%dx%d", gtk_widget_get_width(data->area), gtk_widget_get_height(data->area)); plsetopt("geometry", str); } // change to white background and black foreground, and gray color palette // http://plplot.sourceforge.net/docbook-manual/plplot-html-5.15.0/color.html // seems should be put before plinit; or need pladv(), plvpor(), plwind() for a new picture? // more see example 16 // http://plplot.sourceforge.net/examples.php?demo=16 // /usr/share/plplot5.15.0/examples/c/x16c.c plspal0("cmap0_black_on_white.pal"); plspal1("cmap1_gray.pal", 1); // Initialize plplot // Max 2 plots per line if more than 2 plots if(plot_counts>2) plstar(2,(plot_counts+1)/2); else plstar(1,plot_counts); pl_cmd( PLESC_DEVINIT, cr ); // 10 char unix time: 1745925459 // set numbers of digits to display x axis labels plsxax(10,0); // smaller character size, so x axis characters won't stick together //plschr(0.,.5); //pltimefmt("%m-%d %H:%M:%S"); // show seconds will make x axis characters stick together pltimefmt("%m-%d %H:%M"); for(int i=0;i<(VOLTLEN+TEMPLEN);i++) { if(!active_checkbutton[i]) continue; if(iunix_time_toggle))) plenv(xmin, xmax, ymin, ymax, 0, 0); else // use date / time labels plenv(xmin, xmax, ymin, ymax, 0, 40); if(icans.head; while(pnode!=NULL && ((pnode->item.t <= xmax) || gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(data->live_toggle))) ) { if(pnode->item.t >= xmin) { if(iitem.t,pnode->item.volt[i],1.,0.,0.5,"#(727)"); else plptex(pnode->item.t,pnode->item.temp[i-VOLTLEN],1.,0.,0.5,"#(727)"); } pnode=pnode->next; } } } // Close PLplot library plend(); } // Print last seconds data to command line static void print_data (GtkWidget *widget, gpointer user_data) { DATA *data=user_data; printf("%u",data->cans.end->item.t); for(int i=0;icans.end->item.volt[i]); for(int i=0;icans.end->item.temp[i]); putchar('\n'); } static void set_from_time (GtkWidget *widget, gpointer user_data) { DATA *data=user_data; if(!(data->from_time_entered)) data->from_time_entered=true; // TODO: consider bad input // assume good input, assume no empty input data->from_time=strtoul(gtk_editable_get_text(GTK_EDITABLE(data->from_entry)),NULL,10); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(data->last_seconds_toggle),FALSE); gtk_widget_queue_draw(data->area); } static void set_to_time (GtkWidget *widget, gpointer user_data) { DATA *data=user_data; //if(!(data->to_time_entered)) // data->to_time_entered=true; // set last_seconds_toggle first, because live_toggle will check if last_seconds_toggle is true gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(data->last_seconds_toggle),FALSE); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(data->live_toggle),FALSE); // TODO: consider bad input // assume good input, assume no empty input data->to_time=strtoul(gtk_editable_get_text(GTK_EDITABLE(data->to_entry)),NULL,10); gtk_widget_queue_draw(data->area); } static void update_drawing_area (GtkWidget *widget, gpointer user_data) { DATA *data=user_data; gtk_widget_queue_draw(data->area); } static void live_toggle_callback(GtkWidget *widget, gpointer user_data) { DATA *data=user_data; if(!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(data->live_toggle)) && gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(data->last_seconds_toggle))) { gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(data->live_toggle),TRUE); return; } gtk_widget_queue_draw(data->area); } static void last_seconds_toggle_callback(GtkWidget *widget, gpointer user_data) { DATA *data=user_data; if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(data->last_seconds_toggle))) gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(data->live_toggle),TRUE); gtk_widget_queue_draw(data->area); } // Setup all the GUI widgets. Another better way maybe is use a .xml file instead. // Connect signals between widgets and callback functions. static void activate (GtkApplication *app, gpointer user_data) { DATA *data=user_data; GtkWidget *window; data->area = gtk_drawing_area_new (); GtkWidget *button; GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL,1); GtkWidget *scrolled_window; GtkWidget *flowbox_volt = gtk_flow_box_new(); GtkWidget *flowbox_temp = gtk_flow_box_new(); GtkWidget *expander_volt = gtk_expander_new("Choose cell voltages to plot"); GtkWidget *expander_temp = gtk_expander_new("Choose cell temperatures to plot"); GtkWidget *from_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL,0); GtkWidget *to_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL,0); GtkWidget *from_button = gtk_button_new_with_label ("Enter from unix time"); GtkWidget *to_button = gtk_button_new_with_label ("Enter to unix time"); GtkWidget *flowbox_top = gtk_flow_box_new(); GtkWidget *flowbox_expander = gtk_flow_box_new(); //GtkWidget *expander_more = gtk_expander_new("More"); data->from_entry = gtk_entry_new(); data->to_entry = gtk_entry_new(); // TODO: gtk_entry_set_placeholder_text() //gtk_entry_set_placeholder_text(GTK_ENTRY(data->from_entry),"..."); // unix time now is about 10 char gtk_entry_set_max_length(GTK_ENTRY(data->from_entry),10); gtk_entry_set_max_length(GTK_ENTRY(data->to_entry),10); gtk_box_append(GTK_BOX(from_box), data->from_entry); gtk_box_append(GTK_BOX(from_box), from_button); gtk_box_append(GTK_BOX(to_box), data->to_entry); gtk_box_append(GTK_BOX(to_box), to_button); g_signal_connect (from_button, "clicked", G_CALLBACK (set_from_time), user_data); g_signal_connect (to_button, "clicked", G_CALLBACK (set_to_time), user_data); data->volt_label=gtk_label_new("min/avg/max voltage:"); data->temp_label=gtk_label_new("min/avg/max temperature:"); data->live_toggle = gtk_toggle_button_new_with_label("Live"); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(data->live_toggle),TRUE); // TODO: better live_toggle logic and state consider other states // live_toggle logic not perfect, if consider its states and other states will make logic very complex, so not using live_toggle_callback for now g_signal_connect (data->live_toggle, "toggled", G_CALLBACK (live_toggle_callback), user_data); //g_signal_connect (data->live_toggle, "toggled", G_CALLBACK (update_drawing_area), user_data); data->unix_time_toggle = gtk_toggle_button_new_with_label("Unix time"); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(data->unix_time_toggle),FALSE); g_signal_connect (data->unix_time_toggle, "toggled", G_CALLBACK (update_drawing_area), user_data); data->last_seconds_toggle = gtk_toggle_button_new_with_label("Last 50 seconds"); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(data->last_seconds_toggle),TRUE); g_signal_connect (data->last_seconds_toggle, "toggled", G_CALLBACK (last_seconds_toggle_callback), user_data); for(int i=0;i<(VOLTLEN+TEMPLEN);i++) data->checkbutton[i]=gtk_check_button_new_with_label(checkbutton_names[i]); gtk_check_button_set_active(GTK_CHECK_BUTTON(data->checkbutton[0]),TRUE); gtk_check_button_set_active(GTK_CHECK_BUTTON(data->checkbutton[VOLTLEN]),TRUE); window = gtk_application_window_new (app); gtk_window_set_title (GTK_WINDOW (window), "remote_plot"); //gtk_window_set_default_size (GTK_WINDOW (window), 1000, 1000); //gtk_window_fullscreen (GTK_WINDOW(window)); gtk_window_maximize (GTK_WINDOW(window)); //gtk_drawing_area_set_content_width (GTK_DRAWING_AREA (data->area), 600); //gtk_drawing_area_set_content_height (GTK_DRAWING_AREA (data->area), 600); gtk_drawing_area_set_draw_func (GTK_DRAWING_AREA (data->area), draw_function, user_data, NULL); gtk_widget_set_vexpand(data->area,TRUE); gtk_widget_set_hexpand(data->area,TRUE); button = gtk_button_new_with_label ("Print data"); g_signal_connect (button, "clicked", G_CALLBACK (print_data), user_data); // https://gitlab.gnome.org/GNOME/gtk/-/blob/main/demos/print-editor/print-editor.c scrolled_window=gtk_scrolled_window_new(); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),GTK_POLICY_AUTOMATIC,GTK_POLICY_AUTOMATIC); gtk_scrolled_window_set_has_frame (GTK_SCROLLED_WINDOW (scrolled_window), TRUE); gtk_widget_set_vexpand (scrolled_window, TRUE); gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scrolled_window), box); for(int i=0;i<(VOLTLEN+TEMPLEN);i++) { if(icheckbutton[i]); else gtk_flow_box_append(GTK_FLOW_BOX(flowbox_temp), data->checkbutton[i]); } gtk_expander_set_child (GTK_EXPANDER (expander_volt), flowbox_volt); gtk_expander_set_child (GTK_EXPANDER (expander_temp), flowbox_temp); gtk_flow_box_append(GTK_FLOW_BOX(flowbox_top), button); gtk_flow_box_append(GTK_FLOW_BOX(flowbox_top), data->live_toggle); gtk_flow_box_append(GTK_FLOW_BOX(flowbox_top), data->volt_label); gtk_flow_box_append(GTK_FLOW_BOX(flowbox_top), data->temp_label); gtk_flow_box_append(GTK_FLOW_BOX(flowbox_top), from_box); gtk_flow_box_append(GTK_FLOW_BOX(flowbox_top), to_box); gtk_flow_box_append(GTK_FLOW_BOX(flowbox_top), data->unix_time_toggle); gtk_flow_box_append(GTK_FLOW_BOX(flowbox_top), data->last_seconds_toggle); gtk_flow_box_append(GTK_FLOW_BOX(flowbox_expander), expander_volt); gtk_flow_box_append(GTK_FLOW_BOX(flowbox_expander), expander_temp); gtk_box_append(GTK_BOX(box), flowbox_top); gtk_box_append(GTK_BOX(box), flowbox_expander); gtk_box_append(GTK_BOX(box), data->area); gtk_window_set_child (GTK_WINDOW (window), scrolled_window); gtk_window_present (GTK_WINDOW (window)); read_data(user_data); //printf("before g_timeout_add\n"); // Call read_data function every 1 second g_timeout_add(1000,read_data,user_data); //printf("after g_timeout_add\n"); } // https://gitlab.gnome.org/GNOME/gtk/-/blob/main/demos/gtk-demo/main.c // Setup all the command line things static int command_line (GApplication *app, GApplicationCommandLine *cmdline, gpointer user_data) { DATA *data=user_data; int rc; GVariantDict *options = g_application_command_line_get_options_dict (cmdline); // not sure if using char * here is ok char *dest="Spartan_Racing_Charger@10.0.0.9"; // https://docs.gtk.org/glib/gvariant-format-strings.html#pointers // &s copy the pointer g_variant_dict_lookup (options, "destination", "&s", &dest); g_variant_dict_lookup (options, "local", "b", &data->local); // Only try to ssh connect to the server if I did not specify local on command line if(!(data->local)) { // Open session and set options data->session = ssh_new(); if (data->session == NULL) exit(-1); ssh_options_set(data->session, SSH_OPTIONS_HOST, dest); // Connect to server rc = ssh_connect(data->session); if (rc != SSH_OK) { fprintf(stderr, "Error connecting to %s: %s\n", dest, ssh_get_error(data->session)); ssh_free(data->session); exit(-1); } // Verify the server's identity // For the source code of verify_knownhost(), check previous example if (verify_knownhost(data->session) < 0) { ssh_disconnect(data->session); ssh_free(data->session); exit(-1); } // Authenticate ourselves // https://api.libssh.org/stable/libssh_tutor_authentication.html // I choose to only use public key authentication rc = ssh_userauth_publickey_auto(data->session, NULL, NULL); if (rc != SSH_AUTH_SUCCESS) { fprintf(stderr, "Error authenticating with key: %s\n", ssh_get_error(data->session)); ssh_disconnect(data->session); ssh_free(data->session); exit(-1); } data->sftp = sftp_new(data->session); if (data->sftp == NULL) { fprintf(stderr, "Error allocating SFTP session: %s\n", ssh_get_error(data->session)); return SSH_ERROR; } rc = sftp_init(data->sftp); if (rc != SSH_OK) { fprintf(stderr, "Error initializing SFTP session: code %d.\n", sftp_get_error(data->sftp)); sftp_free(data->sftp); return rc; } } // https://discourse.gnome.org/t/gio-application-commandline-does-trigger-activate-signal/2988/22 // > If you pass G_APPLICATION_HANDLES_COMMAND_LINE in the flags, the // > command-line args are passed to the primary instance and ::activate is // > not emitted. An example again is an something like --hide-window or // > --do-without-disturbing-user which wouldn’t work if the default were to // > trigger ::activate and open the window. So just pass // > G_APPLICATION_HANDLES_COMMAND_LINE and then call // > g_application_activate() in your ::command-line handler g_application_activate(app); return 0; } int main (int argc, char **argv) { DATA data; GtkApplication *app; int status; // Initialize filename to $XDG_DATA_HOME/mycan.csv. If no XDG_DATA_HOME // environment variable, then initialize to $HOME/.local/share/mycan.csv { // usually ~/.local/share const char *dir=getenv("XDG_DATA_HOME"); const char *str; if(dir != NULL) str="/mycan.csv"; else { str="/.local/share/mycan.csv"; dir=getenv("HOME"); } data.filename=malloc((strlen(dir)+strlen(str)+1)*sizeof(char)); sprintf(data.filename,"%s%s",dir,str); } // Initialize linked list InitializeList(&data.cans); if(ListIsFull(&data.cans)) { fprintf(stderr,"No memory available! Bye!\n"); exit(1); } data.from_time_entered=false; //data.to_time_entered=false; data.offset=0; data.local=false; data.just_start=true; //printf("after init\n"); // if I don't handle command line //app = gtk_application_new ("org.gtk.example", G_APPLICATION_DEFAULT_FLAGS); // if I handle command line app = gtk_application_new ("org.gtk.example", G_APPLICATION_HANDLES_COMMAND_LINE); g_application_add_main_option(G_APPLICATION(app),"destination",'d',0,G_OPTION_ARG_STRING,"ssh destination","Spartan_Racing_Charger@10.0.0.9"); g_application_add_main_option(G_APPLICATION(app),"local",'l',0,G_OPTION_ARG_NONE,"Only read from local .csv file",NULL); g_signal_connect (app, "activate", G_CALLBACK (activate), &data); g_signal_connect (app, "command-line", G_CALLBACK (command_line), &data); //printf("before g_app run\n"); status = g_application_run (G_APPLICATION (app), argc, argv); g_object_unref (app); //printf("before ssh free\n"); if(data.local==false) { ssh_disconnect(data.session); ssh_free(data.session); } // Segmentation fault (core dumped) error, why? maybe double free? so no free is ok? //sftp_free(sftp); EmptyTheList(&data.cans); free(data.filename); return status; }