210 IX: IX Software Family: requires( function. . .) II (210.html)

Keywords

ICH180RR requires ISO9003 import prepIX.py show.py "version control" v ixv "function programmer" "user programmer" _v03 isShow "IX: PrepIX" ISO9003_Software_Version_Log.txt "Required Functions List" Functions Macro Entities "Repositories of Functions" OOP Object-Oriented-Programs object module Repositories "Source code" "main program" &ix| nShowDefs "IX Software Family" Bookworm eDict ix_isShow ix_eDict.txt IXp_v2c.py /home/pi/Desktop/IX_assets/ix   _ix.py| showIt_ix.py showIt_ix.py| showIt_ix.py? &ix| method "object method" TM1S "PiNEBERRY Pi" SABRENT 2230 "HatDrive TOP" "HatDrive BOTTOM" PCIe "M.2 HAT" Geekbench glmark2 RISC-V StarFive "VisionFive 2 Computer" "Debian 12 OS" "Lichee Pi 4A" "embedded resistor" "PiNEBERRY Pi FPC" "Flexible Printed Circuit" FPC "MVMe 2242 B+M-Key MakerDisk SSD - 128GB" ix_data_pkg.txt IX-Control

/KeywordsEnd

(To enlarge .....Click it)
thumb: IXimage.jpg
IX or (IX by DC) or "|><"


This article is a part of the IX family of software.

Introduction

The "requires" function is an integral part of the IX Software Family. More is said about the whole IX Software Family in Source 1 below.

	The author apologizes for the complexity of the "requires" 
	functionality.  But powerful software concepts are seldom very 
	simple to understand and use.
	
The "requires" function is a very powerful extension of the important "import" statement normally provided by Python. The improved features of requires (over import) are:

	1) "requires" adds a function to a main program by adding actual 
		visibly numbered statements to the code.
	2) "requires" can be used to enforce version control (import 
		lacks all version control).
	3) "requires" uses "hash totals" to reduce or even eliminate 
		accidental code changes.
	4) "requires" can be told to look for functions in a package 
		external to the current folder.
	5) prepIX "explains" some of the alternate versions that are/were 
		available to the "requires" function
	       ("requires" even accepts a demand that a lower-numbered 
		function version be used e.g. v==2, v>=2 v<2 are all 
		permitted, [but v!=2 cannot be used].  See the 
		    # ***LIST OF REQUIRED IX FUNCTION VERSIONS*** 
		comments at the bottom of the resulting program.)
	6) "requires" can be told to use the latest unknown (highest 
		numbered) version of any single function.
	7) "requires" can be told to use a specific version of a 
		function. If it doesn't exist, of course, the program 
		will NOT run, due to a missing function.
	8) "requires" can be told to actually load the highest numbered 
		version pair of each and every function.  This 
		facilitates debugging of the final resulting program 
		without doing a separate run testing each possible 
		version number of a "required" functions.
	9) The result is a long self-contained Python program that has
		expanded (generated) all the macro code and that 
		contains all of the functions in one large module
		that can be processed by a Python interpreter or
		by Thonny.  Debugging is facilitated because each
		statement has been numbered.
	
	To address its complexity, the author will implement
	the "requires" functionality in two distinct levels, 
	steps or phases when releasing prepIX.

	In Phase I (Source 02) "requires" will only accept "v==0" and will 
	only accept the definition of "requires" as built-into prepIX_I.  
	("v==0" loads only version 0 of each function).  This facilitates
	the most simple way of implementing the IX requires functionality. 
	The Phase II version of requires will be invoked by the program   
	prepIX. The program prepIX is actually version 2 of prepIX.  This 
	departure from normal version control allows the user to code
	the simpler name "prepIX" when requesting the more advanced version
	of "prepIX".

	At any time the user programmer can use the "simple" version of 
	"prepIX" called prepIX_I. This is done by using "prepIX_I" to 
	preprocess the program before running it through Python.
	
	

Drawbacks of the "requires" function

Two slight drawbacks exist when using "requires":

1) when "requires" is told to "function merely as import" using code from a package, every function in the package (ie all code in the whole package) is checked for syntax errors. A recently changed function (even it is unreferenced ie unused by the program being preprocessed) with syntax errors in the package may prevent execution of the resulting program. This may surprise a sloppy (or neophyte) programmer. The solution is to correct the syntax or remove the offending module from the package.

2) when "requires" is told to "function merely as import", the function-pair requirement of "requires" might become problematic and be misunderstood by a neophyte programmer.

The final working versions of Phase II of the "requires" function and the "prepIX" program (that makes use of it) have not yet been written as of 2023KNov12.

Functions, Macros and Entities will be described later in this article, but the "requires" function will be fully explained first. Note that all macros are fully processed by prepIX before functions are added (as "required") by prepIX.

The "requires" Function

In Phase I, the "requires" function itself, needs to be imported using the following import statement:

	from requires import requires
	
For full functionality, the "requires" function itself will be "required" by a "requires" statement.

The "requires" function has (at least) the following 4 main purposes or uses:

A. import-like purpose

The running of the "requires" function will do a similar job to that of an import statement except that the requested function definition statements will be inserted in front of (at the top of) the main program. This will be automatically done by the prepIX.py program. The "requires" function (formerly known as the deprecated "requests" function) can also provide version verification of the "required" function at run-time. The "requires" funtion can be told to use the "import" function instead. The import will not list the imported functions in the resulting program but they will still be executed.

B. version verification

Proper use of the "requires" function causes prepIX to verify that the version of the function that is being required is recent enough. The requested version number of the function will be passed to the "requires" function as variable "v". The number in variable "v" will be compared with the version of the function that will be inserted by prepIX. If the version being inserted is very old, the run-time program will notify the user of the error. Optionally, an error condition will be raised or a warning issued.

This function verification is accomplished by the invocation of two related functions [eg show() and show_v03() ]. The user invokes the "requires" function, telling it the name of the "required" function and optionally the minimum version number needed. The prepIX program uses the name of the required function to look up "show.py" in the folder(s) and packages of functions. The programmer of the main program is responsible to ensure that the "requires(show. . . )" invocation specifies the latest version of show() that is needed by the program. Hopefully, the latest version of the actual function will be found (named in "show.py") by prepIX and will be inserted as a prepend to the module containing the "requires" statement. The prepIX program must also look for the same function name with a suffix of "_v0x" where x is the version of the function. It is this "versioned function" that must contain the actual code or algorithm to be done by the function. The exact algorithm in version 3 of show() will be the actual code in "show_v03.py". If the main program is requesting the same version or lower than version 3, the "requires" function version verification will be satisfied. But if the "requires" statement in the main program requires a higher version than specified, an error (or at least a warning) should be raised by the "requires" function. The next paragraphs will explain how this version verification is accomplished.

In the development of software a number of different versions of a function are often generated over the lifetime of a function. In the IX Family of software, each version of the software should be maintained in the following THREE distinct places:

1) the actual new version of the module. The version number must be part of the name of the module. In this example, the name of version 3 must be show_v03.py. The code in the module will certainly differ from the code in version 2, the previous version. The version number must be assigned to the variable ixv in the module ( e.g. ixv = 3 is needed). The author of the function can make this version number known to the "user programmer" in any way or ways that he/she wishes.

2) the function actually called by the main program must NOT have the version number embedded in its name. But the version number must appear in the function ( ie in show.py) twice. First it must be returned to the "requires" function when requested. More will be said about this elsewhere. The function actually called (directly by the main program) must invoke the correct versioned name of the fuction. In this case, the main program invokes the function show(). The function show() must invoke show_v03(). In fact, show_v03() embodies the major purpose (algorithm) of the function name that the user knows and invokes in the main program. In the case of the "real" show_v03() function, to accomplish its purpose or algorithm, it needs 3 parameters: the string name of the variable to show, the variable itself and a boolian variable (isShow) telling the show() function whether to show anything or not. Usually, the exact same characters are used for the first two parameters, but the first parameter must be enclosed in double quotes. If the name of the variable to be displayed by show() is "firstName", then the full show statemenst in the main program might be:

	isShow = True
	firstName = "Henry"
	show("firstName", firstName, isShow)
The requires() statement that is located near the top of the main program also needs to know the lowest acceptable version of show for the main program. But this "lowest acceptable version number" does NOT need to appear every subsequent time show() is invoked in the main program. It is only the "requires()" statement that needs to know the "lowest acceptable version number". The "requires()" doesn't need to know the actual parameters that will be used when requires is NOT used to invoke show(). For this reason, the actual format (syntax) of the "requires()" statement for the "show()" function is:

	requires("show(s,s,s,'v>=3')")
	
If the program doesn't need the version of show() to be verified, the following "requires" statement should be used:

	requires("show(s,s,s)")
	
That is really all that the user programmer needs to do. Later in the main program, he/she can code
	isShow = True
	firstName = "Henry"
	show("firstName", firstName, isShow)
	
Later a macro named showIt_ix.py will be explained (that will simplify the invocation of the show function). That is all that the "user programmer" using the 'requires("show. . .")' statement needs to know: the syntax when he invokes show() and the lowest (or exact) version number of show() that his program needs. However, the "function programmer", if he/she is not the "user programmer", must know how to code the show_v03.py function and maintain the documents containing the version number. The "function programmer" must also know where to change the version number specified in the show.py module that the "user programmer" invokes.

This may be very complex to understand, but version control of software must not be taken lightly. If this is unclear, get assistance with your version control procedures. Unfortunately, most communities using Python do NOT pay enough attention to software version control. Version control is so important that the best corporate ISO standards demand that it be addressed, be given priority and be maintained as shown next.

C. ISO9003 - version control

Therefore, the third place where the version number of software must be maintained is in the "ISO9003_Software_Version_Log.txt" file. A prerelease version of it can be seen in Source 08. In fact, a future version of prepIX might be able to use this list to automatically create the function that is actually invoked by the main program (eg show.py). For example, the latest version # of the show() function (which is currently 3) should be maintained in an ISO file in a format similar to the following:

ISO9003 Versions (Software Modules)
*************************************************************************
Software Module
fileName/progName	ver Parms Hash	date(YMD)	Description
-----------------	-   ----- ----	----------	------------
. . . . 
show.py			3   3     N	2023KNov01	function used to optionally display the value of a variable.
. . . .

Latest version of ISO9003_Software_Version.txt is version 12 dated 2023KNov02

/ISO9003_Versions.txt
*************************************************************************
It would be a simple matter for prepIX to grep the highest version number of a function from the file above. The prepIX module would also need to retrieve the normal number of parameters which have been included in a column. A useful future column could contain the folder where the function is located. Knowing these two numbers, the prepIX module could automatically create the first of the function pair. Note that even the ISO9003 version text file must also have it's version number maintained.


In ISO9003_Versions.txt in Source 08, many programs function together within a single group, system or project. The following recent Software systems or projects (by the author) are referenced:

	benchmark . . .
	Blinks LED on GPIO 12
	Environ (part of)
	GED
	hash
	ix Family
	linux help
	main file version list
	onboard
	pgm: age by Tristan Marcoux
	pir2
	RPi help
	sandBox
	ssd
	test of function
	textpack
	utility
	
Now, return back to the use of the prepIX.py program.

If more than one version of the show_v0x.py function is found, the prepIX program should select the highest version available. The actual "requires" statement can be left almost "asis" (perhaps without the double quotes) at run-time code. Such a statement can be used at run-time to verify that the version of the required function is recent enough. The "user programmer" will "code" the function name at least twice: once in the text string in parameter 1 of the "requires" function and also each time the function is invoked in the program. The minimum version (if it to be verified) must appear in the form "v>=0" as the second parameter of the "requires" function in the main source code.:

	requires("funcName(s,s,s,'v>=0')")
	
Sometimes, a version change causes the parameters or functionality of the function to change sufficiently that the program will only function correctly with a particular version of the function say version 2. In this case, the following "requires()" statement for the show function, at run-time, would probably be:

	requires("show(s,s,s,'v==2')")

	or (after prepIX processes the "requires" function

	requires(show(s,s,s,'v==2'))
	
In most of the paragraphs above, the boolian version sent to requires has been "=", even though "==" should be used. Most users will wish to economize on keystrokes. Careful programmers will use proper Python boolian expressions.

D. Version list of required functions used in the main program

The prepIX program will also generate a list of the required functions and their actual versions. This list appears as comments at the bottom of the source code generated by prepIX. This list does NOT include the names of any macros used. This list might be:

	# ***LIST OF REQUIRED IX FUNCTION VERSIONS*****
	# Highest	Loaded	Required	   MD5
	# Vsn Avail.	Vsn 	Vsn		   hash
	# ----		----	------------------ ----
	# v02		v02	requires(s,'v==2') Pass
	# v04		v01	show(s,s,s,'v==1') Pass
	# v03		none	inputWto(s,'v==4') No
	# *********************************************
Note that a boolian condition ("==") should be "sent" to the requires function. A single "=" is accepted but is NOT recommended. Note that the version number specified for "requires" has a completely different meaning than the version number specified for a function in a "requires" statement invocation. The following version numbers (and some non-numbers) "sent" to the "requires" function are available:
	v>=0 of requires will use the lowest version of each function 
		that is available regardless of the exact condition 
		specified for the function in the 'requires("func . . )'
		statement.   This may result in run-time (execution) 
		errors, but it will show the user-programmer the results 
		when the lowest version number of every function is used. 
	v==0 of requires will use version 0 of each function that is 
		available. regardless of the exact condition specified.  
		This certainly cause run-time (execution) errors if 
		version 0 of any function is not available.
	v==1 of requires will not exceed the highest function version 
		of each function that is requested unless the condition 
		specifies that a higher version is permitted. 
	v==2 of requires will follow each function's version request 
		"two(pun to) the letter". This means that the 
		boolian phrase "required" for each function will be 
		strictly used.
	v<=99 of requires will use the highest version of each function 
		that is available regardless of the exact condition 
		specified for the function in the 'requires("func . . )'
		statement.   This may result in run-time (execution) 
		errors, but it will show the user-programmer the 
		results when the highest version number of every 
		function is used. This results in the use of the 
		highest known version of the function (ie the most 
		recent) or by default, any available unversioned 
		function of that name to be used. 
	v=="import" causes prepIX to follow version control but
		make use of the Python import facility instead. The
		exact version specified for each function will
		be used, no version errors are expected (usually
		by exhaustive prior testing). The version specified 
		for each function will be followed when the pair
		of functions are specified by "requires" and
		subsequently imported by Python. The benefit of
		doing this is that the statements of each "imported" 
		function will not appear in the final module that
		is run, usually making the resulting code much 
		simpler to understand. This "import" mode can
		be very simply turned on/off by changing
		one statement in the external ix_eDict.txt 
		module.  The statement is:

			ix_isUsePythonImport= True
				or
			ix_isUsePythonImport= False

		This does not change the functions that are used
		It only changes whether or not the statements in
		each of the "required" functions accompany the 
		statements in the main program.  This control
		often simplifies debugging by including OR
		excluding ALL of the function statments in
		the main program. Of course, the pair of 
		functions will still be used/run regardless of
		the boolean status of this control entity.

	v=="none" causes no version control to be used.  
		Each requires("func(. . .") statement is simply 
		converted by prepIX (making no use of version 
		numbers)v into: 

			from func import func
		
		This will NOT result in proper use of the function 
		pairs found in ix_pkg.py.  But it does permit a 
		frustrated neophyte user programmer to take baby 
		steps towards using prepIX macros and the "requires" 
		function.  The neophyte programmer might also 
		appreciate the (LIST OF REQUIRED IX  FUNCTION VERSIONS) 
		and the the detailed program analysis provided by the 
		nShowDefs.py located in the sandBox and in the 
		IX_assets folders in the IX Family of Software.
	v=="" causes prepIX to almost completely ignore automatic 
		version control. Each requires("func(s. . . v=1)") 
		statement is converted into:

			# requires("func(s, 'v=1')")
			from ix_pkg import func
			from ix_pkg import func_v01

		This will use the functions found in ix_pkg.  In this 
		case, the user programmer should soon modify 
		each and every 'v==n' and "func(. . . v=n)" 
		phrase into a function with a valid version.  The 
		prepIX program will still produce the "requires 
		version usage" table at the end of the main program.  
		This table will greatly assist the user-programmer 
		in adjusting the function names that will be 
		partially generated as "func_v0x" in the import 
		statements.  If the programmer is not familiar 
		with (does not know) the most current versions of 
		the functions used, this is the procedure to be 
		followed. During development and testing of "requires" 
		and "prepIX" the author intends to make much use of 
		this facility.

	null	The absence of any mention of "v==" causes the 
		action by requires to be performed as if 
		"v<=99" were coded.  This results in the use of 
		the highest known version of the function (ie the 
		most recent) or by default, any available unversioned 
		function of that name to be used. This action is 
		very close to completely ignoring the version 
		control capability of "requires".
	
If the user programmer wishes the version condition to be absolutely followed to the letter, he/she should specify 'v==2' for requires as shown above. If he/she wishes that the highest version available of every function be used, he/she should specify 'v==0' for requires. Requiring 'v==1' for requires will prevent using a higher version of a program than the version specified. If a version problem is suspected, the more rigorous version of requires ('v==2') should be used. If in doubt, a user programmer should use 'v==2' for requires. Use of 'v==4' for requires is reserved for any special algorithm coded by a function programmer. For example 'v==4' might cause every use of "requires" to cause a normal "import" to be generated. This option ('v==4') has NOT been coded into the IX family of software.

As can be seen the "requires" function and the prepIX program can be used together as one of the most important components of the IX family of software. However, the "requires" function can be completely disregarded, if the user programmer wishes to produce less complex programs.

Written Code vs Generated Code

A Python program is made up of a list of statements commonly referred to as "code".

Most code is written by a human programmer. Each program is usually unique. Because of this, the programmer must write most of the statements that are in the main module.

Some code in a program can be automatically generated or inserted into the main module without the programmer coding it line-by-line. There are at least three types of generated code:

Functions, Macros and Entities

FUNCTIONS are COMPLETE FUNCTION DEFINITIONS beginning with a "def" statement and followed by a function name. It is possible to import a function before it is referenced. The statements that comprise imported functions are not visible in a listing of the main program. Each function definition or import statement must be physically located in the main module in front of (physically preceding) any statements that reference the function. Usually such a reference actually invokes the function. A function can be a routine that returns one or more pieces of information, but it is not imperative that a function return any value or anything at all. Functions not returning anything are often called subroutines. Functions usually terminate their execution of a thread with a "return" statement that returns run-time control (or the thread) to the program that invoked the function. Each time a function is invoked, the exact same set of statements in the function are used, however the thread through a function might take a different path through the function. A program execution thread usually travels completely through a function although sometimes threads can be created or interrupted. A function is presented with zero or more input parameters each time it is called. The values of these parameters can often be different each time the function is called (invoked). The actions taken by a function are determined by these input parameters and by other information that the function encounters or gathers during its execution.

MACROS generate one or more lines of code before the code begins to run. Such lines of code are defined in a macro definition module. Some people say that macros are created by a pre-processor before the final program is generated. These lines of code normally do NOT begin with a "def" and do not normally contain a "return" statement. Some macros only generate data, variables or entities, in which case an execution thread might not be thought to travel through a macro. A single macro might generate vastly different code each time it is invoked to generate code. A macro always generates full lines of code, not partial lines of code. At code generation time (before run time), a macro can be presented with one or more entity definitions. Each entity is usually a small string of characters or a number whose value remains constant during the evaluation phase of the macro. An entity is defined in more detail in the next paragraph. Based on the content of each entity, the macro can generate very different lines of code, or perhaps no code at all. After the evaluation phase is complete, the resulting (generated) code is finalized by the preprocessor and does not change during the execution of the program.

ENTITIES are usually assigned a value by the programmer before the macro modules are converted into code. A macro can be cited in various different instances (even multiple times) in a program. Usually each instance is different. It is the value of the entities used by a macro that cause the macro to generate different code. In IX macros, the value of an entity can be defined in one macro and used in another macro. If an entity is invoked but not defined at all, it is seen as an error and causes an error to be thrown during the evaluation phase of the macro preprocessor, although the IX software could be changed to simply consider an undefined entity as a null string. On any code line that invokes a macro, most of the entities used by that macro will have been defined by the programmer. Some entities can be predefined by a macro or by the programmer before they are used to generate code. In IX macros, entities can be defined in one macro and used in that macro or in another macro. Some macro systems set all entity values to "undefined" at the end of each macro generation instance, IX macros don't do this. In fact, an IX macro might only assign values to entities and might not generate any code at all. All entity values are forgotten immediately after the last macro is used to generate code. In IX macros, the format of an entity name is "&item|". Every entity must begin with "&" and an alphabetic character, then any alphanumeric characters (and a few others eg "_" and "-") are allowed followed by a solid vertical bar ("|") as the last character of the name. An entity name must NOT include a LF. The value assigned to an entity must NOT include a LF. Programmers using IX macros should avoid entity names that begin with "&ix_" because such names are reserved for use by the IX macro system.

Invocation of a Function, an Entity or a Macro

A function and/or a macro can be invoked or used multiple times in a program. Each such use is called an invocation of the function or macro. The flow of control (sometimes called the thread of execution) always passes through a function or a macro. Even when data is defined, the flow of control will pass through these statements. A function or a macro can be mentioned (or used) once or multiple times in a program or in another function or macro. Each such occurence is called an invocation of the function or macro.

The use of a function name that is not preceded by "def " always generates exactly one statement that is sometimes referred to as a function call or invocation. Each invocation of a macro can generates zero, one or more statments.

Zero, one or more parameters can be passed into a function. An example of a function invocation that is passed exactly three parameters is shown below:

	show("firstName", firstName, isShow)
	
Often a function will return one or more results. The results of a function are usually placed into variables by a statement that contains a single "=" character. Usually, there is only one result, but zero or any reasonable number of results are permitted. These results immediately follow the word "return" (prior to the line feed at the end of the return statement) within the function definition as shown below:

	return firstName
	
A macro causes statements to be generated, no variables are ever returned. A macro is never invoked in a statement immediately followed by an "=" character (although "=" signs can appear later in the statement that invokes a macro). However entities can be passed into an invoked macro in a similar but not identical manner to the passing of parameters into a function. A programmer begins the definition of a macro by assigning the macro a "macro file name" followed immediately by a "|". A macro definition must be immediately followed by an invocation of this macro. The macro invocation can be followed by an entity definition, but a preceding macro definition file-name must be followed by a "|". An example of a macro invocation (being passed one entity definition) immediately preceded by its macro definition is shown below:

	showIt_ix.py|
	show("&nam|",&nam|,isShow)
	showIt_ix.py?nam=firstName
	
If the correct macro definition is predefined, found and used by the preprocessor named prepIX, the macro invocation shown above will be used to generate the following line of code. During macro invocation, the preprocessor will examine the definition of the macro named showIt_ix.py . It will find and replace every occurance (in the macro definition) of the five characters "&nam|" with the character string "firstName" which is 9 characters long because the double quotes are not part of the search-string nor the replacement-string.

	show("firstName",firstName,isShow)
	
Once having been defined, either in the main program or external to the main program, the macro definition named showIt_ix.py can be invoked again. The following statement (immediately below) will be analyzed by prepIX:

	showIt_ix.py?nam=lastName
	
After analysis, prepIX will replace it by the (generated) single statement shown below:

	show("lastName",lastName,isShow)
	
Only a few keystrokes are economized in this case, but the macro concept can provide much efficiency for any programmer. Note that the name of the macro ("showIt_ix.py") must be stated when the macro is invoked, even if the macro is only defined in the main program. Furthermore, the macro name can be totally different from the actual characters that are defined in the macro definition (and hence those generated by prepIX.)

Note especially that the full file name (eg showIt_ix.py) of the macro definition must be used when invoking the macro, even if the macro does not appear in any folder nor package.

It is important to fully understand the difference between a macro definition and a function definition. The definition of a function must alway occur in the program's code (either using a "def" statment or at least be mentioned in a "requires" or "import" statement) before it is first invoked. But, the definition of an IX macro does not normally contain the letters "def " and usually occurs immediately before the first time it is invoked. But a macro need not be defined in the program where it is invoked. Instead, the macro definition (especially a longer macro re-used by more than 1 main program) can be stored in the current folder where the main program is stored, or in a library of packaged modules.

Some Python functions can be invoked without being previously mentioned. These are called built-in Python functions. Examples are int() and exit(). Python permits access to external libraries containing functions. Such functions must be mentioned in an "import" statement before being invoked. The programmer is permitted to create IX functions that are stored in a folder or package. These functions must be mentioned in a "requires" statement in the program module before they are referenced (invoked). The prepIX preprocessor will search for all "required" functions in special folders external to the source code module. The prepIX preprocessor will also accept macro definitions defined within the main program or defined in folders outside the main program. The prepIX program will recognize an invocation of a macro by the suffix of the macro name which is "_ix.py" that is NOT immediately followed by a "|". Following "_ix.py" by a LF or EOL is recognized as an invocation of a macro.

	 WARNING - programmers must NOT code the string "_ix.py|" 
		unless defining a macro
	
If a programmer absolutely needs to code "_ix.py|" when NOT defining a macro, it can usually be coded as "_ix.py"+"|" instead. The prepIX preprocessor will recognize the end of a macro definition by the invocation of the macro, or by the string "&ix|" (followed by a LF) which indicates the end of a macro definition. The prepIX program searches for both external functions and external macros using exactly the same search algorithms and search paths.

Searching for external macros and functions

Some IX macros can be defined external to the program where they are invoked. The prepIX preprocessor will search elsewhere for all macros that are invoked without being defined immediately before the first invocation. The prepIX preprocessor will search for such macro definitions in folders external to the main code module.

Whenever a program is being preprocessed or run through Python, a CLI statement can be used to initiate this process. Such a CLI statement can accept the names of files containing code segments (modules) before the name of the main source code module (which must be the last module named). All external function definitions or external macro definitions appearing in the CLI statement will be concatenated to (before) the code in the main source code. An example of such a CLI statement is shown below:

>$ IXp -python3 showIt_ix.py show.py main.py

This CLI statement will cause python3 to evaluate, concatenate and run these three modules. This will cause the showIt_ix.py macro definition module and the show.py function definition module, containing the show() function definition, to be concatenated to (in front of) the main.py module. All three modules: the showIt_ix.py, show.py and main.py modules should be located in the current folder. See Source 04 for more information about IXp. The latest working version, IXp_v2b.py, can be found in Source 05. It is functional and can process a program (see the example below) with an in-line macro, but is still under development:

	 WARNING - programmers must NOT code the string "_ix.py|" unless defining a macro
	

Repositories of IX Functions (and IX Macros)

Functions are a very important, even critical, part of Python programming. Functions are a major part of the software created when using most if not all programming languages.

Macros are similar to Functions, but are not an essential part of Python. Macros simpify programming tasks. Python permits use of a special type of variable called an "object". An object is a new types of variable; and special object methods are functions that only work on objects. Objects can be imported in a similar manner to functions. Programs written to make major uses of objects are called Object-Oriented-Programs (OOP). Objects, methods and OOP will not be explained further herein.

In Raspberry Pi Python programming, there are at least 7 different repositories of functions:

   1. functions inherent to Python (approximately 80 built-in 
	Python functions) e.g.  exit(), int(), input(), len(), 
	print(), range() etc.  All other functions must either 
	be imported or defined within the main program code The 
	statements defining the Python function are not visible 
	to the user.
   2. functions in external libraries (accessible to all users 
	of python). They must be "imported" into a module to be used.
	     import time, sys, GPIO
	e.g. time.sleep(1), sys.exit(), GPIO.output(4,1)
	 All imported functions are not visible in the main program 
	code.
  *3. in-line functions that accompany the source code of the main 
	program. These are defined by the user. Statements defining 
	each of these functions appear as main program code and 
	are visible to the user
  *4. functions stored in the current folder on the user's computer
	These functions are either written by or possessed by the 
	user These are visible to the user
  *5. functions stored in user-accessible folders on the user's 
	computer. These functions are often written by or possessed 
	by the user. These are visible to the user
  *6. packages containing groups of functions. Groups of functions 
	combined on a computer and restricted to specific users.  
	Each Python system does this differently.
   7. functions stored in folders with user-defined paths
	Code "sys.path.append('folder')
	  for a  user-defined system path.
	Other system paths can also be defined.
A Raspberry Python programmer can add an additional repository folder by prefixing his main program code (for example) with the statements shown below. As shown, it would be appropriate to also include the statement to import the "requires" function.
	import sys
	sys.path.append("/home/pi/Desktop/IX_assets/ix")
	from ix_pkg import requires
	#from ix_pymain_pkg import Test_CD2
	
Many, many of the author's Python programs are stored in Desktop/IX_assets/ix/ix_pymain_pkg.py. Most do not use version numbers. They have all been packed (stored) in the ix_pymain_pkg.py package to reduce clutter on any Raspberry boot drive, whether it is a microSD card or an SSD. The fourth line (above) is a comment to illustrate how easy it is to access and run a single Python program of the IX Family of Software. Note that when specifying a function to be imported (or required), the extension ".py" MUST NEVER be specified. However to specifiy a macro, the suffix and extension of "_ix.py" MUST ALWAYS be specified.

An early small version of ix_pkg.py can be seen in Source 07.

The IX Software supports only those repositories marked with an asterisk in the above list.

IXp processing and running the macro test1_ix.py.

The program, IXp_v2c.py can be run in Terminal using the following CLI statements:

	> $ python3 IXp_v2c.py
	IXp |$ -python3 test1_ix.py?count=1,count2=2
	
The macro processor, IXp_v2c.py, will process macro program "test1_ix.py" in Source 06 (shown below):

	# program test1_ix.py
	# by D@CC on 2023KNov11
	&| &count| =5 &|
	&| &count2|=4 &|
	# most recent entity values are used i.e.
	print("ent.values in this macro def. will be used")
	# becomes
	# program test1.py
	print("count:"+str(&count|)+":")
	print("count2:"+str(&count2|)+":")
	print("before for . . &count|+&count2|")
	for i in range(&count|+&count2|):
	    print("for loop:",i+.1)
	    print("In for, count2:"+str(&count2|)+":")
	#for end
	
The macro processor, IXp_v2c.py, generates the resulting program "test1.py" in Source 09 (shown below):

	# program test1_ix.py
	# by D@CC on 2023KNov11
	# most recent entity values are used i.e.
	print("ent.values in this macro def. will be used")
	# becomes
	# program test1.py
	print("count:"+str(5)+":")
	print("count2:"+str(4)+":")
	print("before for . . 5+4")
	for i in range(5+4):
	    print("for loop:",i+.1)
	    print("In for, count2:"+str(4)+":")
	#for end
	
When IXp_v2c.py runs, it produces both its own output and the output of test1.py . Both outputs (concatenated together) are shown below:

	IXp |$-python3 test1_ix.py?count=1,count2=2
	at 112 before eoP, eDict: {'count': '1', 'count2': '2'}
	at 413,cnt, ix_lineIn:5,# most recent entity values are used i.e.:
	at 509, before close, eDict: {'count': '5', 'count2': '4'}
	at 520 ***Running2: python3 test1.py *********************
	ent.values in this macro def. will be used
	count:5:
	count2:4:
	before for . . 5+4
	for loop: 0.1
	In for, count2:4:
	for loop: 1.1
	In for, count2:4:
	for loop: 2.1
	In for, count2:4:
	for loop: 3.1
	In for, count2:4:
	for loop: 4.1
	In for, count2:4:
	for loop: 5.1
	In for, count2:4:
	for loop: 6.1
	In for, count2:4:
	for loop: 7.1
	In for, count2:4:
	for loop: 8.1
	In for, count2:4:
	end of IXp_v2c.py
	
The above is valid output. One issue remains to be resolved. It would be more desirable if the macro entities in the CLI command be treated as the more recent entities, instead of the parameters embedded in the macro. Two "dumps" of the internal entities dictionary named "eDict" were produced (above) to illustrate this issue. This will soon be corrected.

Internal IX-Control entities defined in ix_eDict.txt

The correction will be accomplished as follows. When Python appends items to a dictionary, it replaces any existing item that has the same name. The correction that will be introduced will be to NOT allow InLine entity definitions to replace entities currently in the entity dictionary NOR those in the CLI. Then the entity definitions in the CLI will effectively "over-ride" the entity definitions in the macro. Perhaps, this may be a little confusing, but it is the result currently desired by the author of IXp. Eventually (later in Phase II) it is the intention of the author to allow the programmer to control some alternative modes of execution for IXp. These will be in the form of internal IX-Control entities such as:
ix_eDictDate= 2024BFeb07 ix_EntityPriorityFromCLI= 6 ix_EntityPriorityFromEDictTxt= 5 ix_EntityPriorityFromInLine= 4 ix_EntityPriorityDefault=3 ix_isDisplay_eDict= True #ix_isDisplay_eDict= False ix_isIncludeTrailingIDstamp= True ix_isLimitRequiresTo_v0= True ix_isPrependRequiredFunctions= False ix_isShow= False ix_isSupressComments= True ix_isUsePythonImport= False ix_sp= " " myName= DavidCole myEMail= David4ColeCanada@gmail.com myCellPhone= 613-875-7767 Raspberry= Pi4B /ix_eDict.txt stored in Desktop/IX_assets/ix
There is a source in article 211.html that provides the latest version of the ix_eDict.txt file.

Entity Priorities

At time of writing entities have 4 priority levels. These levels correspond to how recently the programmer has assigned the entity a value. The oldest code in which an entity has been assigned a value is when default values were assigned to an entity. This often happens when initial (default) values are assigned to an entity in a macro. This might be in an initialization macro, for example. The next time a programmer might assign a value to an entity is when a macro is invoked. Each time a macro is invoked, an entity with a different value might be assigned or no value might be assigned. If no value is assigned, the default value is often used. At a later time, the programmer might wish to over-ride both the "FromInLine" and "Default" without changing the existing code. He/she can do this by changing the IX-Control file called ix_eDict.txt that is a file external to the program. (Being external, it is quite easy to change.) At the last minute, the programmer might wish to over-ride all existing entity values. He/she can do this by assigning a value to an entity in the CLI statement. These entity priorities correspond approximately to how long ago the code was adjusted (by choice of entity value). They certainly correspond to how easy it is to adjust the entity value. Of course, changing the actual code is probably not the easiest way to do this. The entity priorities currently in ix_eDict.txt convey these priority concepts. It is recommended that they not be adjusted. At some future date, examples of different entity value assignments should be added to this documentation.

The programmer using IXp and prepIX (Phase II) will then be permitted to change the internal IX entity named "ix_EntityPriorityFromInline" (above) to something other than 4. This will be allowed to be defined in the macro body before the in-line macros are encountered during the execution of IXp. The logic values of the internal IX-Control entities shown above are those that will be implemented in prepIX (Phase II). They will NOT be adjustable by the programmer in Phase I. These internal IX-Control Entities will simply be located in the internal eDict maintained by IXp. It would be good to allow the user of IX to optionally see the contents of eDict (the entity dictionary) during the creation of the resulting program. The contents of eDict could easily be loaded by IXp at run-time from a file named "ix_eDict.txt" located in the Desktop/IX_assets/ix folder. The most recent values of this file (for Phase I) can be seen in Source 10. The ix_isDisplay_eDict could be toggled on/off in two succesive statements to display the contents of eDict. This ix_eDict.txt could serve a second purpose and be a source of user-defined IX data (name, address etc) that could be accessed by macros and by other Python programs, using tools such as grep. It is suggested that the entity for name be myName which would be coded as entity "myName_ix.py" or simply as "myName= DavidCole" in ix_eDict.txt . Note the absence of a space separating "David" and "Cole". Restricting the ix_eDict.txt file to be located only in the "Desktop/IX_assets/ix" folder will make ix_eDict.txt very easy to discover on any Raspberry Pi that makes use of the IX Software Family.

The author of the IX Software Family has included a special IX-control entity named "ix_isShow". Eventually, this IX-control entity will influence the operation of the show() function. The IX-control entity named "ix_isShow" in the external control file named ix_eDict.txt will be examined at the beginning of each main program. This value can be used to turn the isShow variable on/off. In this manner, this debugging information can be turned on or off by this "switch" that is external to the code.

The list of the most important files (most being packages) can be found in Source 23. The name of this list is:
	IX_data_pkg.txt
	

Other Related Thoughts

Install an SSD on the Raspberry Pi 5B using the PiNEBERRY TM1S HAT

Source 03 describes how to upgrade from Bullseye OS to Bookworm OS on a Raspberry Pi in late 2023. But it recommends doing a New Install rather than upgrading. I recently did a new install of 32 bit Bookworm OS on my SABRENT 2230 SSD. I thought that the 64 bit version of Bookworm would still need many changes to become very compatible with the recently released Raspberry Pi 5. This is because the 64 bit Bullseye is said to be the only OS that works on the Raspberry Pi 5.

(To enlarge .....Click it)
thumb: HatDrive.jpg
TM1S "HatDrive TOP" (top view above left & center, bottom view imm. above left & center)


(To enlarge .....Click it)
thumb: IMG_0088.jpg
TM1S "HatDrive TOP" mounted on a RPi 5B ( shown imm. above)


In Source 12 Les Pounder describes the first M.2 HAT (by PiNEBERRY Pi) that adds SSD drives with M key (not SATA) via the PCIe on the RPi 5. Two versions exist (shown above), one mounts on top of the RPi 5, the other on the bottom (photos exist in Source 13 and Source 24). The Sabrent 2230 will mount on the top. The top-mounted board is model TM1S and is called the "HatDrive TOP" (with a price in the range of US$ 20). Les says "The boards also have their own I2C EEPROM that communicates with the Raspberry Pi 5 for identification and configuration." Speeds are similar to SATA speeds (i.e. only a little better than USB 3.0). Additional info can be found in a short article by archyde in Source 13. A third (more detailled) review of the PiNEBERRY Pi M.2 HAT by Adam at picockpit.com can be found in Source 15. Video Source 03 (Explaining Computers) describes the PiNEBERRY Pi M.2 HAT Drive Bottom (for the longer SSD cards). Here is a link to this new (Hat Drive Top) Polish product: PiNEBERRY Pi website . A high-resolution image of the TM1S HatDrive Top by PiNEBERRY Pi appears below left.

The author recently received a PiNEBERRY top-HAT (its actual photo is shown above right). First, I loaded the 64-bit version of BookWorm OS for the RPi 5B onto the SSD by following the instructions by Pi Hut in Web Source S207:11 . I did this using a Raspberry Pi 4B. Installation of the PiNEBERRY Hat was very, very easy by following Video 1 by Jeff Geerling. Only the first two lines (shown below) were editted. I did not change config.txt at all. I have not yet run the power management software provided by PiNEBERRY because it could not be found. In fact, I had no difficulties at all installing the PiNEBERRY, even though I am only using a USB-C power supply designed for the RPi 4B, so I have purposely kept my RPi 5B power usage very low. To conserve power, I have not yet increased the PiNEBERRY speeds by adjusting the Gen=3 parameter. I saw some initial power usage warning messages but the RPi 5B was able to provide enough power to drive a 256 GB KIOXIA (Toshiba) 2230 SSD.

The next PiNEBERRY HAT that I buy will be the bottom version. This will permit me to attach an RPi active cooler Hat on top of the Raspberry Pi 5B. But when using a Bottom-HAT, access to the microSD card is very difficult when using a short FPC "ribbon" cable..

The simple edits needed for using the PiNEBERRY TM1S with the Raspberry Pi 5B are shown below:

	# invoke in Terminal mode by typing:   >$ sudo rpi-eeprom-config --edit
	BOOT_ORDER=0xf416
	PCIE_PROBE=1
	# then save
	#
	# To speed up the PiNEBERRY, 
	#  do the following:
	# invoke in Terminal mode by typing:   >$ sudo nano /boot/config.txt
	dtparam=nvme
	dtparam=pciex1_gen=3
	# then save
	#
	# then reboot by typing:               >$ sudo reboot
	
(To enlarge .....Click it)
thumb: TM1S.jpg
TM1S HatDrive (TOP)


(To enlarge .....Click it)
thumb: hat-pineberry-pi-hatdrive-bottom.jpg
BM1L (BOT) HatDrive Bottom

(Photo courtesy of Jeff Geerling).

An image of the HatDrive Bottom (Source 17) appears above right. It is not known to what extent the RPi 5 GPIO I2C pins are still usable. Jeff Geerling has also created Video Source 1 describing the HatDrives. Be sure to use the PiNEBERRY Pi FPC (Flexible Printed Circuit) PCIe ribbon cable (correctly oriented) to connect the RPi 5 to the HatDrive Top or Bottom. Note that this special "ribbon cable" contains embedded resistors. Jeff explains in detail how to set up and use the HatDrive Top. The SSD drive he used was the Cytron "MVMe 2242 B+M-Key MakerDisk SSD - 128GB" mentioned in Source 20. As always, Jeff's video is a treat to view. His benchmark results are shown below:

(To enlarge .....Click it)
thumb: TMIS_Benchmark.jpg
TM1S Benchmark Results

(Photo courtesy of Jeff Geerling).

A recent article by Les Pounder compared performance of the RPi 5 with 4GB and with 8GB of RAM. His tests showed the 8GB to not be much more productive than the 4GB. He plans to rerun the same tests when 1GB and 2GB versions of the RPi 5 become available.

Source 11 by Patrick explains how to set up a "Headless" Raspberry Pi microSD card. It is extremely simple if you have a Pi-400 available.

Source 14 by Emmet provides many benchmarks of the RPi 5. Comparisons are with the RPi 4 mainly using Geekbench and glmark2. These are free tools runnable by anyone. Emmet even describes how to run them. These benchmarks show the RPi 5 to be about 3 times as fast as the RPi 4. Cooling of the RPi 5 seems to be only a minor issue, but some cooling of the RPi 5 is recommended.

Source 18 is my first mention of the RISC-V StarFive computer with RPi compatible GPIO. It is available from Cytron and runs Debian 12 OS. Video Source 2 by Christopher Barnatt (of Explaining Computers) provides an introduction to this second edition of the VisionFive 2 Computer. RISC-V is becoming an interesting competitor to the RPi computers, although this video (c Sept 2023) still mentions many shortcomings. The video even shows Python software control of some GPIO LEDs.

Source 19 by Matthew Connatser appears to be an initial sign that transistor architecture (image below) may provide thermal relief for "hot" circuitry.

(To enlarge .....Click it)
thumb: ThermalDensityRelief.jpg
Solving Thermal Density

(Image credit: JayzTwoCents/YouTube)

Apparently, this special type of transistor will conduct heat better "when charged with electricity".

Source 21 by Brandon Hill of Tom's Hardware recommends buying a portable monitor during the 2023 Black Friday sales. However, he points out a deficiency of Raspberry Pi computers. He says "However, your device must support [DisplayPort Alt-Mode over USB-C] as most recent laptops do, but Raspberry Pi does not". Is this still true with the RPi 5? Brandon Hill's article, at Source 21, compares a number of portable monitors that are currently available.

The name of the manufacturer of the fasteners for the author's kitchen drawers in Canada is Ovis.

Recently, on vacation, I forgot to pack an HDMI-to-USB-micro cable, making my Pi-400 unusable. I wish that I could use a Pi boot program that would blink a LED to tell me the IP address of the Pi-400. This would permit me to use a laptop to operate the Pi-400 in a headless mode. Damn!

Did you ever wonder how to turn on/off a led on the Pi-400 keyboard? Source 22 by Aaron Fisher explains how to turn off the power LED on an RPi 4 but . . . .

See Source 26 for my choice of a 3D printed case for the Raspberry Pi 5: the Model 642650 at printables.com . I requested that the latest version be printed (named Current version-v3). The choices of tops and bottom were: top_full.stl and bottom.stl . Hopefully the choice of top will have openings for the GPIO pins and future ribbon cables.

(To enlarge .....Click it)
thumb: 642650.jpg
3D Model 642650

(Image credit: pyrho at printables)

Source 27 by Wagner is an excellent source of recent information about the Raspberry Pi 5B. Its directory is shown below. Wagner even shows how to increase the screen resolution from 720p to 1080p. He also describes how to test proper functioning of the fan. In a video, Wagner describes how to set up the CanaKit containing the Raspberry Pi 5B.

	Table of Contents

	1 Helpful Resources
	2 Q&A
	3 Specifications
	4 Feature Breakdown
	5 Where to Buy a Pi
	6 Accessories
		Official
		General
		3rd Party
	7 Complete Pi 5 Kits
		CanaKit Raspberry Pi 5 Starter Kit
	8 Using the Raspberry Pi 5
	9 Touch Screen
	10 Pi Desktop
		Quick How-To's
		Update from Terminal
		Add Software
		Change Screen Resolution
		Safely Shutdown the Pi 5
		  Method 1
		  Method 2
		  Method 3
		  Force Shutdown (Unsafe Method)
		Stress Test
		Helpful Terminal Commands
	11 Accessibility
	12 Ubuntu Desktop
		Quick How-To's
		  Pin Applications to the Toolbar
		  Add Software
		  Locate Applications
		  How to Update Ubuntu
		  Change Screen Resolution
		  Safely Shutdown the Pi 5
	13 Troubleshooting
	14 Change Log
	

PCIe and HAT+ Specifications

The MagPi magazine has announced preliminary specifications for the PCIe and HAT+ for the RPi 5B. Links to them are available in the article in Source 28. They will pertain to the M.2 HAT which is planned for early 2024 (shown below).

(To enlarge .....Click it)
thumb: M2_HAT.jpg
Future M.2 HAT for the Raspberry Pi 5B

(Image credit: MagPi)

Sources

Video Sources

Video Source V210:01: FINALLY! NVMe SSDs on the Raspberry Pi (13:30m) by Jeff Geerling c 2023 K Nov 16
Video Source V210:02: Lichee Pi 4A: Serious RISC-V Desktop Computing(19:13m) by Christopher Barnatt of Explaining Computers c 2023 I Sep 16
Video Source V210:03: Raspberry Pi 5 M.2 HatDrive(15:48m) by Christopher Barnatt of Explaining Computers on 2023 L Dec 31
Video Source V210:04: This blows away the $60 budget oscilloscope(OWON VDS1022 review)!(58:39m) by Adrian's Digital Basement on 2023 A Jan 21

Web Sources

Web Source S210:01:www requires_v01_py.txt by D@CC on 2023JOct26
Web Source S210:02:www 209 IX: IX Family of Software: requires() Phase I (209.html) D@CC on 2023KNov07
Web Source S210:03:www How to Upgrade Raspberry Pi OS from Bullseye to Bookworm by RaspberryTips on 2023KNov08
Web Source S210:04:www IT: IX/IXc - A General Purpose Macro Processor (183.html) by D@CC on 2023KNov11
Web Source S210:05:www recent IXp_v2b_py.txt (text) by RaspberryTips on 2023KNov08
Web Source S210:06:www test1_ix_py.txt (text) by D@CC on 2023KNov09
Web Source S210:07:www ix_pkg_py.txt by D@CC on 2023KNov03
Web Source S210:08:www ISO9003_Versions.txt by D@CC on 2023KNov09
Web Source S210:09:www test1_py.txt (text) by D@CC on 2023KNov09
Web Source S210:10:www ix_eDict.txt by D@CC on 2023KNov11
Web Source S210:11:www How to Set Up a Headless Raspberry Pi by Patrick on 2023KNov17
Web Source S210:12:www Add SSD to RPi 5 with a first M.2 HAT by Les Pounder on 2023KNov16
Web Source S210:13:www RPi 5 M.2 SSD UPGRADE: PiNEBERRY Pi Hatdrive by archyde on 2023KNov18
Web Source S210:14:www Exploring Benchmarks of the Raspberry Pi 5 by Emmet on 2023KNov12
Web Source S210:15:www An M.2 PCIe HAT for RPi 5 by Adam on 2023KNov17
Web Source S210:16:www HatDrive! Top (NVMe 2230, 2242 GEN 3) for RPi 5 by PiNEBERRY Pi c 2023KNov18
Web Source S210:17:www HatDrive! Bottom (NVMe 2230, 2242, 2280 GEN 3) for RPi 5 by PiNEBERRY Pi c 2023KNov18
Web Source S210:18:www VisionFive 2 8G by Cytron c 2023KNov18
Web Source S210:19:www Solving thermal density with new transistor architecture by Matthew Connatser 2023KNov14
Web Source S210:20:www Cytron MVMe 2242 B+M-Key MakerDisk SSD - 128GB by Cytron before 2023KNov19
Web Source S210:21:www Connectivity Options with Portable Monitors by Tom's Hardware on 2023KNov17
Web Source S210:22:www Turning Off the Power LED on an RPi 4 by Aaron Fisher on 2022 A Jan 02
Web Source S210:23:www IX_pkg_info.txt by D@CC on 2023KNov11
Web Source S210:24:www PiNEBERRY HatDrive Raspberry Pi 5 M.2 NVMe SSD HAT . . . by Julian Horsey on 2024 A Jan 02
Web Source S210:25:www The Best Raspberry Pi 5 Cases to 3D Print . . by Gloria E. Magarotto on 2024 A Jan 03
Web Source S210:26:www Model 642650 at printables.com by pyrho on 2024 A Jan 17
Web Source S210:27:www Raspberry Pi 5 Guide by Wagner on 2024 A Jan 07
Web Source S210:28:www Raspberry Pi PCIe and HAT+ Specifications by "The MagPi" on 2024 B Feb 07

/SourcesEnd

WebMaster: Ye Old King Cole

There is a way to "google" any of the part-numbers, words or phrases in all of the author's articles. This "google-like" search limits itself ONLY to his articles. Just go to the top of "Articles by Old King Cole" and look for the "search" input box named "freefind".

Click here to return to Articles by Old King Cole

Date Written :2023 K Nov 02
Last Updated:2024 D Apr 05

All rights reserved 2024 by © ICH180RR

saved in E:\E\2022\DevE\MyPagesE\Globat\ePhotoCaption.com\a\210\210.html
backed up to ePhotoCaption.com\a\210\210_2023KNov19.html

Font: Courier New 10 (monospaced)
/210.html