Saturday, July 09, 2022

Searching for Raspberry Pi's

I would like to get one of those Raspberry Pi Model 4 with at least 4 gig of RAM. They are really kind of hard to get a hold of right now. I know there is that web site out there that you can monitor the Raspberry Pi retailers, but I don’t want to have to check a web site every hour hoping to catch one for sale. I wanted to also have a way of monitoring them that uses little cpu and small amount of real estate on my desktop monitor. 

Here is my plan, to write a small GUI application that checks for availability every few minutes and display the results in a small table format. First question is how to check for availability. Well, I could wget each website into a sub-directory and then scan the files for a key word. 

For each retailer (I am only looking at US Retailers right now) the web site will be different and for each web site the key word may also be different. I also discovered that I was going to need a count for how may times I was going to find that key word in the HTML source. This has Array written all over it. An Array for the web sites, an array for the key words and finally an Array for the counts. 

For those who don’t know what an Array is, one my text books describes an Array as Homogeneous data structure, basically that means similar data items are stored under one name. So, basically it is a way to group similar types of data together. It is also the most common data structure in data processing. Each separate item can then be accessed by the name with an index. Each index is a whole number (Integer) which cannot exceed the size of the array. If you exceed the size or use a negative number, the program will usually crash. Arrays can usually be of one simple type of item or a more complex structure. 

I did this in Lazarus, which is a GUI front end to the Free Pascal Language. It is a Form based system that is friendly and very easy to work with. It is also Object Oriented with many visual components. The main ones I will be using here are the Tbutton and TStringGrid. The Tbutton allows the user to start the whole process by just clicking on the button, the TstringGrid allows a structured and pretty way of displaying the info. 

Lets look at the Arrays first. I could have made this one big complex structure, but since they were going to be mostly constants and I thought it would be easier to look at separate simpler basic arrays. Free Pascal is a superset of Turbo Pascal from the early 1980s and then in the late 1980-1990s object oriented data structures were added. Niklaus Wirth’s Pascal from the 1970s didn’t have a string library built in, but Turbo Pascal did and it made a much more usable programming language. Here, you will see that I have used String type in a number of my Arrays.

 

Const
  StoreLabel: Array[1..20] of String = ('AdaFruit', 'CanaKit-1', 'CanaKit-2',
              'CanaKit-4', 'CanaKit-8','PiShop-1', 'PiShop-2','PiShop-4',
              'PiShop-8', 'Okco-2', 'Okco-4', 'Okco-8', 'Chicago-1gb',
              'Chicago-2gb', 'Chicago-4gb', 'Chicago-8gb','SparkFun-2',
              'SparkFun-4', 'SparkFun-8', 'MicroCenter');
  WebAddr: Array[1..20] of String = ('https://www.adafruit.com/product/4295',
           'https://www.canakit.com/raspberry-pi-4.html',
           'https://www.canakit.com/raspberry-pi-4-2gb.html',
           'https://www.canakit.com/raspberry-pi-4-4gb.html',
           'https://www.canakit.com/raspberry-pi-4-8gb.html',
           'https://www.pishop.us/product/raspberry-pi-4-model-b-1gb/',
           'https://www.pishop.us/product/raspberry-pi-4-model-b-2gb/',
           'https://www.pishop.us/product/raspberry-pi-4-model-b-4gb/',
           'https://www.pishop.us/product/raspberry-pi-4-model-b-8gb/',
           'https://www.okdo.com/us/p/raspberry-pi-4-model-b-2gb/',
           'https://www.okdo.com/us/p/raspberry-pi-4-model-b-4gb/',
           'https://www.okdo.com/us/p/raspberry-pi-4-model-b-8gb/',
           'https://chicagodist.com/products/raspberry-pi-4-model-b-1gb?src=raspberrypi',
           'https://chicagodist.com/products/raspberry-pi-4-model-b-2gb?src=raspberrypi',
           'https://chicagodist.com/products/raspberry-pi-4-model-b-4gb?src=raspberrypi',
           'https://chicagodist.com/products/raspberry-pi-4-model-b-8gb?src=raspberrypi',
           'https://www.sparkfun.com/products/15446',
           'https://www.sparkfun.com/products/15447',
           'https://www.sparkfun.com/products/16811',
           'https://www.microcenter.com/product/609038/4--model-b-4gb?src=raspberrypi');
  SearchFor: Array[1..20] of String = ('Out of stock', 'Sold Out',
             'Sold Out', 'Sold Out', 'Sold Out', 'Out of stock',
             'Out of stock', 'Out of stock', 'Out of stock',
             'Out of stock', 'Out of stock', 'Out of stock', 'Sold Out',
             'Sold Out', 'Sold Out', 'Sold Out',
             'do not currently have an estimate',
             'do not currently have an estimate',
             'do not currently have an estimate',
             'UNAVAILABLE ONLINE');
  SearchCnt: Array[1..20] of LongInt = (12, 4, 2, 3, 3, 4, 4, 4, 4, 3, 3, 3,
             1, 1, 1, 1, 2, 2, 2, 1);

The StoreLabel Array is just used to put a label for the retailer, I used a number to specify which version of the Raspberry Pi it was linked to. WebAddr of course has the Web addresses for each retailer and SearchFor has the key words to search for in the HTML source. For anyone wondering about efficiency, Free Pascal does arrays almost as fast as “c” does and easily outperforms Python’s List Processing. Tests I have done in the past indicates “c” arrays run in about one fiftieth of the amount of time as Python does. Pascal requires about four percent more time than “c” does. So “c” and Pascal run neck and neck with array processing and Python is left in the dust.


Now for the logic. When one places a Tbutton on a form in Lazarus, and then double clicks on the Tbutton, Lazarus creates an OnClick Event for the button and also creates a Procedure that will execute at run time when you click on the button. You can also instruct Lazarus to do the same thing by manipulating the Object Inspector window in Lazarus.


The source code looks like the following:


     1	Procedure BuildHeading(Var SG: TStringGrid);
     2	Begin
     3	  SG.Clear;
     4	  SG.ColCount := 5;
     5	  SG.RowCount := 30;
     6	  SG.Cells[0, 0] := 'Store Name';
     7	  SG.Cells[1, 0] := 'How Many';
     8	  SG.Cells[1, 1] := 'Should Be';
     9	  SG.Cells[2, 0] := 'How Many';
    10	  SG.Cells[2, 1] := 'There are';
    11	  SG.Cells[3, 0] := 'Differance';
    12	  SG.Cells[4, 0] := 'Status';
    13	
    14	End;
    15	
    16	Procedure DeleteTempFiles();
    17	Var
    18	  fn:                       String;
    19	  Info:                     TSearchRec;
    20	Begin
    21	  if FindFirst(KkDirStr + '/*.*',faAnyFile,Info) = 0 Then
    22	        Repeat
    23	              fn := Info.Name;
    24	              If ((fn <> '.') and (fn <> '..')) Then
    25	                   DeleteFile(KkDirStr + '/' + fn);
    26	            until FindNext(info)<>0;
    27	        FindClose(Info);
    28	end;
    29	
    30	Procedure StartWGet(Addr: String);
    31	Var
    32	  InternetProcess:                        TProcess;
    33	Begin
    34	   InternetProcess := TProcess.Create(nil);
    35	   InternetProcess.Executable:= 'wget';
    36	   InternetProcess.Parameters.Add('--directory-prefix=' + KkDirStr);
    37	   InternetProcess.Parameters.Add(Addr);
    38	   InternetProcess.Options := InternetProcess.Options + [poWaitOnExit];
    39	   InternetProcess.Execute;
    40	   InternetProcess.Free;
    41	end;
    42	
    43	Function FindKeywordCnt(KeyStr: String): LongInt;
    44	Var
    45	  KeyWordCnt, i, j:        LongInt;
    46	  fn, WorkStr:             String;
    47	  Info:                    TSearchRec;
    48	  F:                       TextFile;
    49	  StrSearched:             Boolean;
    50	Begin
    51	  KeyWordCnt := 0;
    52	  FindFirst(KkDirStr + '/*.*',faAnyFile,Info);
    53	  Repeat
    54	     fn := Info.Name;
    55	     If ((fn <> '.') and (fn <> '..') and (Length(fn) > 2)) Then
    56	     Begin
    57	       AssignFile(F, KkDirStr + '/' + fn);
    58	       Reset(F);
    59	       ReadLn(F, WorkStr);
    60	       Repeat
    61	          i := Length(KeyStr);
    62	          StrSearched := False;
    63	          For j := 1 To Length(WorkStr) Do
    64	          Begin
    65	               If Copy(WorkStr,j,i) = KeyStr Then
    66	                  KeyWordCnt := KeyWordCnt + 1;
    67	          end;
    68	          ReadLn(F, WorkStr);
    69	       Until Eof(F);
    70	       CloseFile(F);
    71	     end;
    72	  Until FindNext(info)<>0;
    73	  FindClose(Info);
    74	  FindKeyWordCnt := KeyWordCnt;
    75	end;
    76	
    77	Procedure DisplayDtlLine(Var SG: TStringGrid; RowVal: LongInt;
    78	          StoreLabel: String; Fnd, ExpCnt: LongInt; Var TimeVar: TDateTime);
    79	Const
    80	  MyEmailAddr = 'MyEmailAddr';
    81	  MyGMailAddr = 'MyGMailAddr';
    82	  MyEmailSvr = 'MyEmailSvr';
    83	  MyEmailUser = 'MyEmailUsr';
    84	  MyEmailPass = 'MyEmailPass';
    85	Var
    86	  Msg:                                    TStringList;
    87	Begin
    88	   Msg := TStringList.Create;
    89	   SG.Cells[0, RowVal+2] := StoreLabel;
    90	   SG.Cells[1, RowVal+2] := IntToStr(ExpCnt);
    91	   SG.Cells[2, RowVal+2] := IntToStr(Fnd);
    92	   SG.Cells[3, RowVal+2] := IntToStr(Fnd-ExpCnt);
    93	   If Fnd < ExpCnt Then
    94	   Begin
    95	      SG.Cells[4, RowVal+2] := 'Available';
    96	      If Now() > IncHour(TimeVar, 3) Then
    97	      Begin
    98	         Msg.Clear;
    99	         Msg.Add(StoreLabel + ' May have RaspPi''s');
   100	         SendToEx(MyEmailAddr, MyGmailAddr,
   101	            StoreLabel + ' May have RaspPi''s', MyEmailSvr,
   102	            Msg, MyEmailUser, MyEmailPass);
   103	         TimeVar := Now();
   104	      end;
   105	   end
   106	   Else SG.Cells[4, RowVal+2] := 'UnAvailable';
   107	   Msg.Free;
   108	End;
   109	
   110	procedure TMainForm.BeginButClick(Sender: TObject);
   111	var
   112	  i, j, k, P, CntFnd:                     LongInt;
   113	  Done, StrSearched:                      Boolean;
   114	  WorkStr:                                AnsiString;
   115	  Info:                                   TSearchRec;
   116	  fn:                                     String;
   117	  EndTime:                                TDateTime;
   118	  TimeArray:                              Array[1..20] of TDateTime;
   119	begin
   120	  Done := False;
   121	  For i := 1 To 20 Do
   122	      TimeArray[i] := Now();
   123	  Repeat
   124	    BuildHeading(RaspPiSG);
   125	    For i := 1 To 20 Do
   126	    Begin
   127	         DeleteTempFiles();
   128	         StartWGet(WebAddr[i]);
   129	         CntFnd := FindKeyWordCnt(SearchFor[i]);
   130	         DisplayDtlLine(RaspPiSG, i, StoreLabel[i], CntFnd, SearchCnt[i],
   131	           TimeArray[i]);
   132	    end;
   133	    RaspPiSG.Cells[0, 23] := 'Last Refresh';
   134	    RaspPiSG.Cells[1, 23] := TimeToStr(Now());
   135	    RaspPiSG.AutoSizeColumns;
   136	    RaspPiSG.Repaint;
   137	    EndTime := Now + EncodeTime(0, 0, 45, 0);   // 45 Seconds
   138	    While Now < EndTime do
   139	    Begin
   140	          Application.ProcessMessages;
   141	          Sleep(10);
   142	    end;
   143	  until Done;
   144	
   145	end;

Lets step through the Procedure and talk about how it works. The TButton Procedure that Lazarus created is clear down on line 110. You can see the actual Procedure is fairly short because it calls sub procedures and functions to do most of the work.


“Done” is first thing set in this procedure, it is a boolean variable which means it can be set to “True” or “False” only. I have set it to “False” and will execute a “Repeat” until Done equals “True”, that will never happen and this procedure will run for ever or at least until I kill the program.


The "For" loop sets an array of TdateTime to the current time(lines 121-122). I want to send myself an email if a Pi becomes available.


Wait … Did I say email???


Well, it’s too easy to miss it on the screen and a message to my phone is a another way to notify that some Pi’s have become available. But I don’t want to send that email every minute or even multiple times per hour, so I am keeping a list of the last time I sent an email and I preset last time email was sent to the startup of the process.


On line 123 we have the Repeat, the Until Done is clear down on line 143. This loop should never end. The first thing I do in the "repeat loop" is call the “BuildHeading Procedure. The “BuildHeading” Procedure is on lines 1-14. The most interesting thing about that procedure is that I pass the RaspPiSG as a Var which allows me to modify it in the called procedure. Note in the called procedure I just refer to it as SG. The names of variables do not have to be the same between the calling and the called procedures, but the types and order of called variables have to align.


On lines 125-132, I execute a for loop, inside this loop I call several procedures and a function.


On Line 128 I call the Procedure “DeleteTempFiles” which is on lines 16-28. Every time we run the Linux program “wget” we download files from a webpage. That directory needs cleaned out before downloading the next retailers website, so I execute a “FindFirst”...”FindNext” and then delete every file it finds. Many languages use similar functions to accomplish something like this.


On line 128, I call the “StartWGet” Procedure (lines 30-41). And that is exactly what it does. It runs “wget” as a background process and control of my program is blocked until “wget” finishes. “wget” dumps the HTML source files into the work directory. This uses a Tprocess data structure defined in the Var section. It also requires “Process be declared in the Uses deceleration which pulls in the correct libraries. “wget” is a standard Linux program used to grab HTML files from websites, many distros have it installed by default. If not, “wget” should be in your distro’s repository. Using Tprocess to execute an external process is explained in the following website:


https://wiki.freepascal.org/Executing_External_Programs


Note the variable we pass to “StartWGet”


WebAddr[i]


is referring to one of the array elements where “i” is an integer. The integer value in “i” combined with the array name refers to a unique element in that array. Using the name “i” for the index for element of an array is kind of historic and goes back to the early days of Fortran.


On line 129, I call the FindKeyWordCnt Function (Lines 43-75). This Function also does a “FindFirst”...”FindNext” loop, but instead of deleting the files, it counts how many times the KeyWord for that specific Retailer is in the HTML files. Finally the Integer is returned to the CntFnd variable.


Finally on line 130-131, I call the DisplayDtlLine Procedure(Line 77-108) which displays the current detail line in our TstringGrid. Note I passed the TstringGrid in this procedure just like I did in the BuildHeading Procedure. In this procedure is where I send an email to my GMail Account. Msg is defined in this Procedure as a TstringList, which is an predefined object defined in the “Classes” library. TstringList is a sort of structured array of strings with some programming logic added. A TStringList is required to send an email through the app, the TStringList contains the email body’s content. If you check out Lazarus, you will find many visual components use TstringList as a Foundational Element. Listboxes, dropdown-boxes and database components all use the TstringList internally.


This is where my for loop ends and we bounce back to finishing the Repeat Statement.


Lines 133-136 causes my TStringGrid to display a little nicer. The columns get sized correctly and the time of last display is printed.


Performance: It doesn’t seem that bad. Watching it through Top, the program never pulls more than a 1% or 2% on one cpu. The only bottleneck is running all of the wget’s in sequence. I could set it up to run the wget’s in batches. If I ran five at a time it would finish executing the wget’s much quicker. It would make the program a little more complex and requires multiple work directories, but it would be doable.


Email: The procedure I use to send email comes from Synapse’s website. You have to download their source-code and add it as libraries to your app. It is totally procedural. http://www.ararat.cz/synapse/doku.php/start

I would like to consider something other than email, GMail is blocking some of my emails.  I assume they think I am doing something impoper. I looked at sending IM’s, but that would cost money and I like free. My Wife suggested I check out Signal, haven’t gotten around to that yet. I would need a good command line method to send the messages and to easily receive them on my phone. The email is actually sent on lines 100-102. I first check to see if an email has been sent recently, if not a new one will be sent. Once a new email is sent, that Retailer’s TimeArray element is updated with the current Date/Time.


Results: I haven’t got a Pi yet. Came close a couple of times. I wrote an additional program that lets me select the site and it pulls up the website. I can get to each website in just a few seconds. The additional program was required because when I put the program to sleep, it makes the program unresponsive. While the second program is very responsive. Microcenter has become a problem. They changed their website and now I haven’t found a good keyword, the keyword that is displayed on the website doesn’t show up in HTML source code that is downloaded. A number of the other websites has changed and I have had to change a few keywords to get good results. This is an image of my program running: