Monday, August 18, 2025

From PC to Physical Computing Powerhouse: Bridging the GPIO Gap


In the world of hobbyist electronics and rapid prototyping, the Raspberry Pi is king. Its secret weapon? The General-Purpose Input/Output (GPIO) header—a row of pins that allows the tiny computer to interact directly with the physical world, from reading sensors to driving motors. But what about the powerful desktop or laptop you use every day? These machines, for all their computational might, are sealed off, lacking a simple, built-in way to blink an LED, let alone interface with complex electronics. For years, this meant that physical computing projects were tethered to specialized single-board computers or microcontrollers.

Enter the Adafruit FT232H Breakout. This unassuming little board is a game-changer, a powerful Rosetta Stone for hardware communication. It acts as a multi-protocol bridge, translating the universal language of USB into the dialects of the electronics world: GPIO, SPI, and, most importantly for our project today, I2C. By simply plugging it into a standard USB port on any Windows, macOS, or Linux machine, you effectively grant your computer the same kind of hardware-level control that has made the Raspberry Pi so beloved. Suddenly, your powerful PC isn't just for processing data; it's for controlling it in the real world.

The Project: A Tale of Two Lines

To demonstrate the power and simplicity of the FT232H, we'll tackle a classic "Hello, World!" project of the hardware realm: driving a standard 2-line by 16-character (16x2) LCD display. These displays are ubiquitous in projects, perfect for showing status updates, sensor readings, or user menus. We'll use an I2C-enabled version, which simplifies wiring to just four connections: power, ground, data, and clock.

What You'll Need:

  • An Adafruit FT232H Breakout board.

  • A standard 16x2 character LCD with an I2C backpack.

  • A breadboard and a few jumper wires.

  • A computer running Python.

The Setup: Wiring it Up

Connecting the components is refreshingly straightforward. The FT232H board has clearly labeled pins. For I2C communication, we only need four wires:

  1. 5V to VCC: Connect the FT232H's 5V pin to the LCD's VCC (power) pin.

  2. GND to GND: Connect one of the FT232H's GND (ground) pins to the LCD's GND pin.

  3. AD1/SCL to SCL: This is the I2C clock line. Connect the FT232H's D1 pin (which also serves as SCL) to the LCD's SCL pin.

  4. AD2/SDA to SDA: This is the I2C data line. Connect the FT232H's D2 pin (SDA) to the LCD's SDA pin.

That's it. No complex schematics, no soldering required. The USB connection from your computer will power both the FT232H and the LCD.

The Brains: Speaking I2C with CircuitPython

With the hardware connected, the real magic happens in the software. We'll use CircuitPython, Adafruit's beginner-friendly version of Python for microcontrollers, along with the adafruit-blinka library, which allows CircuitPython libraries to run on standard computers.

First, you'll need to set up your computer to work with the FT232H. Adafruit provides excellent, detailed guides for this process on their learning system, which involves installing some drivers and Python libraries.

Once your environment is configured, the Python code to control the LCD is remarkably simple. The script below will initialize the I2C connection and display a two-line message, a nod to your interest in high-fidelity audio.

This code is self-contained and ready to run. It handles all the necessary imports, sets up the I2C communication with the FT232H, finds the LCD's address, and displays the text.

The true power here is the abstraction provided by the CircuitPython libraries. Complex I2C protocols are boiled down to simple, readable Python commands. The lcd.message function handles the intricate details of sending character data to the display controller; you just need to provide the string.

Beyond the Display: The Possibilities are Limitless

While driving an LCD is a simple demonstration, it unlocks a vast ecosystem of I2C-compatible sensors and devices. Temperature sensors, accelerometers, motor drivers, and OLED displays can all be controlled from your main computer using the same FT232H board and a few lines of Python.

For developers, testers, and hobbyists, this is a profound shift. You can now write sophisticated Python scripts on your powerful development machine—with all its debugging tools and processing power—to directly interface with and test hardware components. No more writing code on a PC, cross-compiling, and flashing it to a microcontroller just to see if a sensor works.

The FT232H doesn't replace devices like the Raspberry Pi or Arduino, but it powerfully complements them. It tears down the wall between software development and hardware prototyping, turning any computer into a versatile physical computing workstation. It’s a small, inexpensive tool that adds a massive capability to the machine you already own. 

CircuitPython LCD Driver

"""

This script demonstrates how to drive a 16x2 I2C character LCD display

using an Adafruit FT232H breakout board and CircuitPython libraries on a

standard computer.


It initializes the I2C communication, detects the LCD's address, and

displays a two-line message.

"""


import time

import typing

import board

import busio

from adafruit_character_lcd.character_lcd_i2c import Character_LCD_I2C


# Define LCD dimensions

LCD_COLUMNS: typing.Final[int] = 16

LCD_ROWS: typing.Final[int] = 2



def find_i2c_device_address(i2c: busio.I2C) -> typing.Optional[int]:

    """

    Scans the I2C bus for a connected device.


    Args:

        i2c: The I2C bus object.


    Returns:

        The address of the first device found, or None if no device is found.

    """

    while not i2c.try_lock():

        pass

    try:

        addresses: list[int] = i2c.scan()

        if addresses:

            print(f"I2C device found at address: {hex(addresses[0])}")

            return addresses[0]

        print("No I2C device found.")

        return None

    finally:

        i2c.unlock()



def main() -> None:

    """

    Main function to initialize the LCD and display a message.

    """

    # Initialize I2C bus. SCL and SDA are connected to the FT232H's

    # D1 and D2 pins respectively.

    try:

        i2c: busio.I2C = busio.I2C(board.SCL, board.SDA)

    except Exception as e:

        print(f"Error initializing I2C bus: {e}")

        print(

            "Please ensure the FT232H is connected and drivers are "

            "installed."

        )

        return


    # Find the LCD's I2C address

    i2c_address: typing.Optional[int] = find_i2c_device_address(i2c)


    if i2c_address is None:

        return


    # Initialize the LCD

    try:

        lcd: Character_LCD_I2C = Character_LCD_I2C(

            i2c, LCD_COLUMNS, LCD_ROWS, i2c_address=i2c_address

        )

    except Exception as e:

        print(f"Error initializing LCD at address {hex(i2c_address)}: {e}")

        return


    # Turn on the backlight

    lcd.backlight = True


    # Display a message

    # A nod to your audiophile interests!

    line1: str = "Magnepan & Tidal"

    line2: str = "Pure Planar Bliss"

    message: str = f"{line1}\n{line2}"

    print(f"Displaying message:\n{message}")

    lcd.message = message


    # Keep the message displayed for 10 seconds

    try:

        time.sleep(10)

    except KeyboardInterrupt:

        pass

    finally:

        # Clean up

        lcd.backlight = False

        lcd.clear()

        print("\nScript finished. LCD cleared.")



if __name__ == "__main__":

    main()

 Unit Test for LCD Driver

"""

Unit tests for the LCD driver script.


This test suite uses mocks to simulate the hardware (I2C bus and LCD)

to allow for testing the application logic without requiring the physical

devices to be connected.

"""


import unittest

import typing

from unittest.mock import MagicMock, patch, call


# Mock the hardware modules before they are imported by the script under test

# This is crucial for running tests in an environment without the actual

# hardware or CircuitPython libraries installed.

mock_board: MagicMock = MagicMock()

mock_busio: MagicMock = MagicMock()

mock_lcd: MagicMock = MagicMock()


# Create a mock I2C class within the mock busio module

mock_i2c_instance: MagicMock = MagicMock()

mock_busio.I2C.return_value = mock_i2c_instance


# Create a mock LCD class within the mock adafruit_character_lcd module

mock_lcd_instance: MagicMock = MagicMock()

mock_lcd.Character_LCD_I2C.return_value = mock_lcd_instance


modules: dict[str, MagicMock] = {

    'board': mock_board,

    'busio': mock_busio,

    'adafruit_character_lcd.character_lcd_i2c': mock_lcd,

}


with patch.dict('sys.modules', modules):

    # Now we can import the script we want to test

    from lcd_driver_script import (

        main,

        find_i2c_device_address,

        LCD_COLUMNS,

        LCD_ROWS,

    )



class TestLcdDriver(unittest.TestCase):

    """

    Test cases for the LCD driver script.

    """


    def setUp(self) -> None:

        """

        Reset mocks before each test.

        """

        mock_i2c_instance.reset_mock()

        mock_lcd_instance.reset_mock()

        mock_busio.I2C.reset_mock()

        mock_lcd.Character_LCD_I2C.reset_mock()


    def test_find_i2c_device_address_found(self) -> None:

        """

        Test that the I2C scan correctly returns a found address.

        """

        # Configure the mock to return a dummy I2C address

        expected_address: int = 0x27

        mock_i2c_instance.scan.return_value = [expected_address]


        # Call the function under test

        address: typing.Optional[int] = find_i2c_device_address(

            mock_i2c_instance

        )


        # Assert that the lock/unlock sequence was called

        mock_i2c_instance.try_lock.assert_called_once()

        mock_i2c_instance.scan.assert_called_once()

        mock_i2c_instance.unlock.assert_called_once()

        # Assert that the correct address was returned

        self.assertEqual(address, expected_address)


    def test_find_i2c_device_address_not_found(self) -> None:

        """

        Test that the I2C scan returns None when no device is found.

        """

        # Configure the mock to return an empty list

        mock_i2c_instance.scan.return_value = []


        # Call the function under test

        address: typing.Optional[int] = find_i2c_device_address(

            mock_i2c_instance

        )


        # Assert that the correct value (None) was returned

        self.assertIsNone(address)


    @patch('lcd_driver_script.find_i2c_device_address')

    @patch('time.sleep')

    def test_main_full_success_path(

            self, mock_sleep: MagicMock, mock_find_address: MagicMock

    ) -> None:

        """

        Test the main function's successful execution path.

        """

        # Configure the mock to return a dummy I2C address

        test_address: int = 0x3F

        mock_find_address.return_value = test_address


        # Run the main function

        main()


        # Verify that the I2C bus was initialized

        mock_busio.I2C.assert_called_once_with(

            mock_board.SCL, mock_board.SDA

        )


        # Verify that the LCD was initialized with the correct parameters

        mock_lcd.Character_LCD_I2C.assert_called_once_with(

            mock_i2c_instance, LCD_COLUMNS, LCD_ROWS,

            i2c_address=test_address

        )


        # Verify the sequence of operations on the LCD instance

        self.assertEqual(mock_lcd_instance.mock_calls, [

            call.backlight(True),

            call.message('Magnepan & Tidal\nPure Planar Bliss'),

            call.backlight(False),

            call.clear()

        ])


        # Verify that sleep was called

        mock_sleep.assert_called_once_with(10)


    @patch('lcd_driver_script.find_i2c_device_address')

    def test_main_no_device_found(self, mock_find_address: MagicMock) -> None:

        """

        Test the main function's behavior when no I2C device is found.

        """

        # Configure the mock to simulate no device being found

        mock_find_address.return_value = None


        # Run the main function

        main()


        # Assert that the LCD constructor was never called

        mock_lcd.Character_LCD_I2C.assert_not_called()

        # Assert that no message was set on the LCD instance

        mock_lcd_instance.message.assert_not_called()



if __name__ == '__main__':

    unittest.main()


Saturday, August 16, 2025

Tune your audio system to your room

Wiim Mini DAC's Hidden Feature Transforms Listening Experience

​The Wiim Mini DAC, known for its budget-friendly price, hides a powerful feature within its Android app that could revolutionize your home audio. Dubbed "RoomFit," this function uses your Android device's microphone to analyze your listening space and automatically adjust the equalizer (EQ) for a flatter, more accurate sound.

​Buried in the device settings, the process is a two-step affair. The Wiim first plays a sweeping tone across the full frequency range, which is then recorded by your phone's microphone. The app analyzes this data to identify and correct for room-specific acoustic issues. It then automatically applies these EQ settings to the Wiim Mini.
​A second sweep confirms the new settings and offers the option to accept the changes. The results can be dramatic. In one instance, using the Wiim with a pair of powered monitors in a bedroom yielded exceptional results. The same user, an audiophile who owns a pair of Magnepan LRS+ speakers, noted that the difference on their main living room system was nothing short of "amazing."

​This unexpected performance boost was so impressive that an additional Wiim Mini was immediately ordered to make the room correction a permanent fixture in the main listening area.

Thursday, August 14, 2025

What saved info I use to configure Google gemini


 

Google's Gemini AI engine allows you to configure how Gemini interprets your prompts and how you want it to behave. I've seen a huge improvement in the responses I am getting after adding an eclectic set of information. To learn how this information is used see "How is this information is used".

In order to set your own saved info, click on"Settings and Help" at the bottom of the left pane. You can then click the [+ Add] button to add a new information section.

Here are my information sections I've created as an example of the type of information you might want to include.

Friday, August 8, 2025

How to call c functions from python and how to write and prepare the c functions.



Calling C Functions from Python: The Underlying Concept

To call C functions from Python, the general approach involves compiling your C code into a shared library, which Python can then dynamically load and interact with.

  • Rationale: C is frequently used for computationally intensive libraries due to its thin abstraction layer from hardware and low overhead, allowing for efficient implementations of algorithms and data structures. Many higher-level languages, including Python, support calling C library functions for performance-critical or hardware-interacting aspects.
  • Mechanism: Python is designed to be extended with C code, and it's also possible to embed a scripting language like Python within a C program. When extending Python, you instruct it to dynamically load a C library or module, making the C entry points visible as functions within the Python environment. This process requires the ability to transfer data between C's and Python's distinct type systems. C extensions are commonly used in the Python ecosystem.

Writing and Preparing C Functions for Python Interoperability

To prepare your C functions for use with Python, you will typically write them as part of a shared library.

  1. C Language Fundamentals

    • C is a general-purpose, imperative procedural language. It forms the foundation for much of GNU/Linux software, including the kernel itself. The information about C is also applicable to C++ programs, as C++ is largely a superset of C, and C language APIs are the common interface (lingua franca) in GNU/Linux.
    • C source files contain declarations and function definitions. These are typically saved with .c or .h (for header files) extensions.
  2. Defining C Functions

    • C functions typically accept parameters and can return values. For instance, pthreads functions in GNU/Linux use void* for parameters and return types to allow for generic data passing to and from threads.
    • For C++ Code: If you are writing the C functions in C++ that will be part of a shared library and accessed from other languages (like Python), you should declare those functions and variables with the extern "C" linkage specifier. This is crucial because it prevents the C++ compiler from "mangling" function names, ensuring that the function's name remains as you defined it (e.g., foo instead of a complex, encoded name). A C compiler does not mangle names.
  3. Compiling into a Shared Library

    • The GNU C Compiler (GCC) is the standard compiler for C code in Linux.
    • Step 1: Compile Source Files to Position Independent Code (PIC): The first step is to compile your C source files into object code that is "position-independent." This is necessary for shared libraries. You use the -c option for compilation only and the -fPIC option for position-independent code generation.
      • Example command: gcc -c -fPIC your_c_file.c. This will generate your_c_file.o.
    • Step 2: Create the Shared Library: Once you have your object files (.o), you link them together to create the shared library, which typically has a .so extension (for "shared object"). Use the -shared option for this.
      • Example command: gcc -shared -o libyourlibrary.so your_c_file.o.
    • Making the Library Discoverable: For your Python program to find and load this shared library at runtime, it must be located in a directory that the system's dynamic linker searches.
      • You can set the LD_LIBRARY_PATH environment variable to include the directory containing your shared library.
      • Alternatively, you can place the shared library in a standard system library directory, such as /lib or /usr/lib, and then run ldconfig to update the dynamic linker's cache.
  4. Crucial Considerations for Robust and Secure C Code When writing C functions, especially those intended for interoperability, adhering to robust and secure programming practices is vital:

    • Error Handling:
      • Always check the return values of system calls and library functions to detect failures. A return value of -1 often indicates failure for system calls.
      • In case of failure, the errno variable (from <errno.h>) will contain an error number, which can be converted to a descriptive error string using strerror(). The perror() function can also be used to print a generic error message.
      • For functions that allocate resources (like memory or file descriptors), ensure they are deallocated if an error occurs later in the function.
    • Memory Management:
      • C programs manage memory through static allocation, automatic allocation (on the stack), and dynamic allocation (on the heap). When dynamically allocating memory using functions like malloc() or realloc(), always check if the returned pointer is NULL, which indicates allocation failure. Abort the program if memory allocation fails.
      • Remember to free() dynamically allocated memory when it's no longer needed to prevent memory leaks.
    • Input Validation and Buffer Overflow Prevention:
      • Avoid "dangerous" C library functions that do not perform bounds checking, such as strcpy(), strcat(), sprintf(), and gets(). These are common sources of buffer overflow vulnerabilities.
      • Use safer alternatives that allow you to specify buffer sizes, like strncpy(), strncat(), snprintf(), and fgets(). Be aware that strncpy() and strncat() have specific semantics (e.g., strncpy() may not null-terminate if the source is too long). Consider using strlcpy() and strlcat() if available, as they offer a less error-prone interface.
      • Be extremely cautious with the scanf() family of functions, particularly when using %s without specifying a maximum length, as this can lead to buffer overruns.
    • Compiler Warnings and Static Analysis:
      • Always compile your C code with as many compiler warnings enabled as possible. For GCC, use at least the -Wall and -pedantic flags. Strive to eliminate all warnings, as they often indicate potential bugs or risky constructions.
      • Use ANSI prototypes in separate header (.h) files for all your functions to ensure type correctness across your program.
      • For functions that take format strings (like printf()-style functions), GCC's __attribute__((format(printf, parameter_number_of_format_string, first_variadic_parameter_number))) can be used in your header files to enable the compiler to check format string accuracy.
  5. Including Header Files

    • Ensure you include all necessary header files for the functions and system calls you use. Common headers include:

By following these guidelines, you can effectively write and prepare C functions that can be called from Python, leveraging the strengths of both languages.


Once I have my shared library of compiled c code, how do I include it in my python program and call it?

To include your shared library of compiled C code in a Python program and call functions from it, you'll need to leverage Python's capabilities for dynamic loading. This process involves ensuring your C code is correctly compiled into a shared library, making that library discoverable by the system, and then using Python's interface to dynamically load it and call its functions.

Here's a detailed breakdown:

1. Compiling and Preparing Your C Code (Recap)

Firstly, ensure your C (or C++) code is compiled into a shared library, often referred to as a shared object (.so) file on GNU/Linux systems.

  • Compile to Position-Independent Code (PIC): The C source files that will form your shared library must be compiled into position-independent code. This is essential because the library's functions may be loaded at different memory addresses in different programs. You achieve this using the -fPIC option with the GNU C Compiler (GCC).
    • Example: gcc -c -fPIC your_c_file.c.
  • Create the Shared Library: After compiling your source files into object files (.o), you link them together to create the shared library. You use the -shared option for this. Shared libraries typically have a .so extension and their names almost always begin with lib.
    • Example: gcc -shared -o libyourlibrary.so your_c_file.o another_c_file.o.
  • extern "C" for C++ Functions: If you are writing your functions in C++ that are intended to be accessed from other languages like Python, you must declare those functions and variables with the extern "C" linkage specifier. This is crucial because C++ compilers "mangle" function names (encoding extra information like parameter types into the name), which would prevent Python from finding the function by its original name. A C compiler, however, does not mangle names. A common practice is to wrap declarations in header files with #ifdef __cplusplus extern "C" { #endif and #ifdef __cplusplus } #endif.

2. Making the Shared Library Discoverable

For your Python program to load and use your compiled C shared library, the system's dynamic linker needs to be able to find it at runtime.

  • LD_LIBRARY_PATH Environment Variable: One common solution is to set the LD_LIBRARY_PATH environment variable. This variable is a colon-separated list of directories that the system searches for shared libraries before looking in the standard directories. This is particularly useful during development or for non-standard library installations.
    • Example: export LD_LIBRARY_PATH=./:$LD_LIBRARY_PATH (if the library is in the current directory).
  • Standard System Directories and ldconfig: Alternatively, you can place your shared library in one of the standard system library directories, such as /lib or /usr/lib. After placing a new library in these directories, you should run ldconfig to update the dynamic linker's cache, which helps the system find libraries efficiently.
  • -Wl,-rpath Linker Option: Another approach is to embed the library's path directly into your executable at link time using the -Wl,-rpath option. This tells the system where to search for required shared libraries when the program is run.

3. Including and Calling from Python

Python, being extensible with C code, provides mechanisms to dynamically load shared libraries and call functions defined within them [576, 577, previous turn]. While the provided sources do not explicitly name a specific Python module for this (like ctypes), they extensively describe the underlying C-level functions that Python would use: dlopen and dlsym.

The general concept is as follows:

  1. Dynamic Loading: Python will perform an action equivalent to the dlopen function. The dlopen function opens a shared library, returning a handle that can then be used to refer to the loaded library. This allows code to be loaded at runtime without being explicitly linked at compile time.
  2. Symbol Resolution: Once the library is loaded, Python needs to find the specific function you wish to call. This is analogous to the dlsym function in C, which takes the library handle and the name of the function (or variable) as a string and returns its address.
  3. Function Call: With the address of the C function obtained, Python can then invoke it.

Conceptual Steps in a Python Program:

Although the source material doesn't provide direct Python code examples for dlopen/dlsym wrappers, the process would conceptually involve:

  • Importing a Module for Foreign Function Interface: Python has modules (like ctypes in a real-world scenario, but not mentioned in sources) designed for calling C functions. You would import such a module.
  • Loading Your Shared Library: You would then call a function (conceptually like dlopen) to load your libyourlibrary.so file. This would provide a handle to the loaded library.
  • Defining the C Function Signature: To properly call a C function, Python needs to know its signature (return type and argument types). This is crucial for correctly passing data between Python and C's distinct type systems. Python's foreign function interface (FFI) modules handle this by allowing you to specify the C function's prototype.
  • Accessing and Calling the Function: You would then access the C function by its name (conceptually like dlsym) through the loaded library handle. Once accessed and its signature defined, you can call it just like a regular Python function, passing Python objects as arguments. The FFI module handles the conversion of Python types to C types and vice versa.

Example (Conceptual Python Usage):

Assuming you have libmyutility.so containing a function int add(int a, int b); compiled as described above:

# Conceptual Python code (not actual syntax for a specific Python module like ctypes,
# but illustrates the underlying dlopen/dlsym principle as described in sources)

# Step 1: Load the shared library (conceptually using dlopen)
# In reality, a Python FFI module would abstract this.
my_library_handle = python_load_library("libmyutility.so")

if my_library_handle:
    # Step 2: Access the C function (conceptually using dlsym)
    # You would also need to specify argument types and return type
    add_function = python_get_function(my_library_handle, "add")

    # Step 3: Call the C function
    result = add_function(10, 20)
    print(f"The result of add(10, 20) is: {result}")

    # Step 4: Unload the library when done (conceptually using dlclose)
    python_unload_library(my_library_handle)
else:
    print("Failed to load the library.")

This approach allows you to leverage the performance and low-level capabilities of C from the flexibility and high-level environment of Python.

Wednesday, August 6, 2025

How to effectively start a Python script that relies on a virtual environment from a cron job.

This is a common task in Linux system administration, and it requires understanding how cron jobs execute commands and how Python virtual environments are designed to work.

Understanding the Challenge with Cron Jobs and Virtual Environments

When a cron job executes a command, it typically runs in a very minimal environment, often without the full set of environment variables that are present in your interactive shell session (the shell you use when you log in). This is crucial because Python virtual environments rely on setting specific environment variables, particularly modifying the PATH variable, to ensure that the correct Python interpreter and its associated packages are used.

In a typical interactive session, you activate a virtual environment using a command like source /path/to/venv/bin/activate. This command modifies the current shell's environment variables to point to the virtual environment's Python interpreter and its installed libraries. However, a cron job executes commands in a new, non-interactive shell for each scheduled task, and this shell doesn't automatically inherit the environment changes made by your interactive session's activate script.

If you simply try to run your Python script (e.g., python /path/to/your_script.py) in a cron job without properly addressing the virtual environment, the cron job's shell will likely use the system's default Python interpreter, which will not have access to the packages installed in your virtual environment. This would lead to "module not found" errors or unexpected behavior.

The Solution: Explicitly Calling the Virtual Environment's Python

The most robust way to ensure your Python script runs with its virtual environment from a cron job is to explicitly call the Python interpreter within that virtual environment using its absolute path. This bypasses the need to source the activation script, as the virtual environment's Python interpreter is self-contained and knows how to find its own packages.

Here are the steps and examples:

Step 1: Identify the Absolute Paths

You need to know the full, absolute path to:

  1. Your Python script: For example, /home/youruser/my_project/my_script.py.
  2. Your virtual environment's Python interpreter: If your virtual environment is located at /home/youruser/my_project/venv, then the Python interpreter inside it would typically be /home/youruser/my_project/venv/bin/python.

Step 2: Choose Your Cron Job Method

You have two primary ways to set up the cron job: directly in the crontab file, or by using a wrapper shell script. The wrapper script approach is generally recommended for better logging, error handling, and readability.

Method A: Direct Entry in Crontab (Simplest for single commands)

You can add the command directly to your crontab file. To edit your crontab, you typically use the command crontab -e.

The format for a crontab entry is minute hour day_of_month month day_of_week user command. Since you are likely running this under your own user, the "user" field will be implied if you edit your personal crontab (using crontab -e), but it's explicitly required in /etc/crontab.

Here's an example crontab entry that runs your script daily at 9:00 AM:

0 9 * * * /home/youruser/my_project/venv/bin/python /home/youruser/my_project/my_script.py >> /home/youruser/my_project/cron.log 2>&1

Let's break this down:

  • 0 9 * * *: This specifies the schedule (0 minutes past 9 AM, every day of the month, every month, every day of the week).
  • /home/youruser/my_project/venv/bin/python: This is the absolute path to the Python interpreter within your virtual environment. This is key to ensuring the correct environment is used.
  • /home/youruser/my_project/my_script.py: This is the absolute path to your Python script.
  • >> /home/youruser/my_project/cron.log 2>&1: This redirects both standard output and standard error to a log file [This is general shell practice for cron jobs, as they don't have an interactive terminal to show output directly]. This is highly recommended for debugging, as cron jobs run silently unless there's an issue or output is redirected.

Method B: Using a Wrapper Shell Script (Recommended)

For more complex scripts, or if you need to perform additional setup (like changing directories or setting specific environment variables) before running your Python script, a wrapper shell script is a cleaner and more robust approach.

Step 2.1: Create the Wrapper Shell Script

Create a new file, for example, run_my_python_job.sh, in a suitable location (e.g., /home/youruser/scripts/).

#!/bin/bash

# --- Configuration ---
# Absolute path to your Python virtual environment
VENV_PATH="/home/youruser/my_project/venv"

# Absolute path to your Python script
SCRIPT_PATH="/home/youruser/my_project/my_script.py"

# Absolute path for the log file
LOG_FILE="/home/youruser/my_project/cron.log"

# --- Script Execution ---

# Optional: Change to the script's directory (useful if your script
# relies on relative paths for other files)
cd "$(dirname "$SCRIPT_PATH")"

# Execute the Python script using the virtual environment's Python interpreter
# All output (stdout and stderr) is appended to the log file.
"$VENV_PATH/bin/python" "$SCRIPT_PATH" >> "$LOG_FILE" 2>&1

# Optional: You could also explicitly activate the virtual environment
# if your script implicitly relies on other environment variables set by 'activate'.
# This would typically be placed before the Python execution line.
# source "$VENV_PATH/bin/activate"
# python "$SCRIPT_PATH" >> "$LOG_FILE" 2>&1
# deactivate # 'deactivate' is not strictly necessary for non-interactive scripts

Step 2.2: Make the Wrapper Script Executable

You must give the shell script execute permissions.

chmod +x /home/youruser/scripts/run_my_python_job.sh

Step 2.3: Schedule the Wrapper Script in Crontab

Now, add an entry to your crontab that calls this wrapper script:

0 9 * * * /home/youruser/scripts/run_my_python_job.sh

Important Considerations:

  • Absolute Paths: Always use absolute (full) paths for everything in cron jobs: the Python interpreter, your script, and any log files or other resources your script needs to access. The cron environment's PATH variable is often very limited.
  • Logging: Redirecting output (>> /path/to/logfile.log 2>&1) is crucial. Without it, you won't see any print statements or error messages if your script fails. This is your primary debugging tool for cron jobs.
  • Permissions: Ensure your script, the virtual environment, and the log file locations have the correct read/write/execute permissions for the user that the cron job will run as.
  • Environment Variables within Script: If your Python script itself relies on custom environment variables that are not set by the virtual environment activation (e.g., API keys, database connection strings), you will need to explicitly set these within your wrapper script or directly in the cron command line using VAR_NAME=value /path/to/command. Variables set with export in a parent shell are inherited by child processes.

By following these steps, you can reliably run your Python scripts from cron jobs, ensuring they execute within their intended virtual environments.

Friday, July 18, 2025

Using AI to retrieve genre and synopsis from book titles

I have several thousand epub files. They are named in such a way that there is usually the author and title. I can use the epub-metadata library in python to get the title. I’d like to be able to use AI and google to get the genre and synopsys. I have the Google One Pro edition which gives me the Gemini Pro but you could also use it’s free version. You need to go to the Google AIStudio to get your API key. Create an environment variable GOOGLE_API_KEY with your key and the genai library will automatically find it. Otherwise you can pass the API key directly when you create the instance of the client. See Using Gemini API keys for examples. I then created a tiny python script as a proof of concept.

Title: the emperors new mind

('Genre: Science, Physics, Mathematics, Computer Science, Philosophy of Mind, ' 'Artificial Intelligence\n' 'Synopsys: Roger Penrose, a renowned physicist and mathematician, presents a ' 'controversial argument that human consciousness cannot be fully explained by ' 'classical computation or algorithms, directly challenging the foundations of ' "'strong AI.' Drawing upon concepts from quantum mechanics, general " "relativity, and Gödel's incompleteness theorems, he proposes that " 'non-computable processes are essential to understanding the mind, suggesting ' 'a deeper connection between consciousness and the fundamental laws of ' 'physics.')

"""
Google Books API Integration Script

This script uses Google's Gemini AI API to retrieve genre and synopsis information
for books based on their titles. It constructs a query using the book title and
the Google Books URL format, then uses the Gemini AI model to generate a response
with genre and synopsis information.

Requirements:
- Google Gemini API key set as environment variable 'GEMINI_API_KEY'
- google-generativeai package installed

Usage:
    python test_google_book_api.py
    (The script will prompt for a book title)
"""

import pprint  # For pretty-printing the API response
from google import genai  # Google's Generative AI library

# The client gets the API key from the environment variable `GEMINI_API_KEY`.
# Google Books URL format: https://www.google.ca/books/edition/title


def aibook(title):
    """
    Query the Gemini AI model for book genre and synopsis information.
    
    This function creates a Gemini AI client, constructs a prompt with the book title
    and desired output format, then returns the generated text response.
    
    Args:
        title (str): The title of the book to query
        
    Returns:
        str: The formatted response text containing genre and synopsis information
        
    Example output format:
        Genre: Fiction, Science Fiction, Dystopian
        Synopsis: A story about...
    """
    # Initialize the Gemini AI client
    client = genai.Client()
    
    # Generate content using the Gemini model with a formatted prompt
    response = client.models.generate_content(
        model="gemini-2.5-flash",  # Using the Gemini 2.5 Flash model for fast responses
        contents=f"""
        book url https://www.google.ca/books/edition/{title}/
        provide the genre and synopsis of the book
        use the format 
        line 1 Genre: genre1, genre2, etc
        line 2 Synopsys: the synopsys     
        """
    )
    
    # Return the text portion of the response
    return response.text


# Execute only if run as a script, not when imported as a module
if __name__ == "__main__":
    # Prompt the user for a book title, pass it to the aibook function,
    # and pretty-print the results
    pprint.pp(aibook(input('Title: ')))

Saturday, July 5, 2025

You Are Not Expected to Understand This - How 26 Lines of Code Changed the World

This collection of texts explores the **profound impact of code and software on various aspects of society**, from its early conceptualisation to modern-day applications. It discusses how **human decisions, biases, and ingenious solutions are embedded within code**, influencing everything from space exploration and the internet's structure to social interactions and public safety. The sources examine **pivotal moments in computing history**, such as the development of programming languages like BASIC and COBOL, the invention of email, and the rise of digital gaming. Furthermore, they highlight **critical challenges like software vulnerabilities and ethical dilemmas**, including the unintended consequences of algorithms in policing and advertising, and the ongoing struggle for digital privacy and inclusivity. Ultimately, the compilation underscores that **code is not merely technical but a deeply human endeavour** with far-reaching societal implications.

Saturday, June 28, 2025

Raspberry Pi Weather Clock

 I have a 2 line by 16 character LCD display with a I2C interface. I wanted to use it as a clock and date display which also could display the current weather.


Date and Time display
Date and Time Display


Weather Display
Weather Display
I am using a raspberry pi 5 to run a python script to drive the display using the GPIO pins

Rapsberry Pi 5 clock/weather


You will need an API key to open weather openweatherapi. Put your api key in an .env file as openweathermapapikey=yourkey. The python script uses the dotenv library to read this and use your API key. This is a free service with usage limitations. You will need to change the parameters to the open weather API to use you city and units.

        params = {
            "q": "Etobicoke,Ontario,CA",
            "units": "metric",  # For Celsius
            "appid": OPENWEATHERMAPAPIKEY  # API key
        }

#!/usr/bin/env python3
"""
lcd_weatherclock.py - A program that displays a clock on an LCD screen
and shows weather information for 10 seconds when a button is pressed.
"""

from LCD import LCD
import datetime
import time
import requests
import os
from dotenv import load_dotenv
from gpiozero import Button
import threading

# Load environment variables from .env file
load_dotenv()

# Initialize LCD
lcd = LCD(2, 0x27, True)
lcd.clear()

# Initialize button with debounce
button = Button(21, pull_up=True, bounce_time=0.2)  # 200ms debounce time

# Global variables
display_mode = "clock"  # Can be "clock" or "weather"
weather_timer = None

def get_etobicoke_weather():
    """
    Get current weather for Etobicoke, Ontario, Canada using OpenWeatherMap API
    """
    try:
        # OpenWeatherMap API endpoint for current weather
        url = "https://api.openweathermap.org/data/2.5/weather"

        # Get APP ID for weather api
        OPENWEATHERMAPAPIKEY = os.getenv("openweathermapapikey")
        # Parameters for Etobicoke, Ontario, Canada
        params = {
            "q": "Etobicoke,Ontario,CA",
            "units": "metric",  # For Celsius
            "appid": OPENWEATHERMAPAPIKEY  # API key
        }

        # Make the API request
        response = requests.get(url, params=params)

        # Check if the request was successful
        if response.status_code == 200:
            # Parse the JSON response
            weather_data = response.json()

            # Extract relevant information
            temperature = weather_data["main"]["temp"]
            condition = weather_data["weather"][0]["main"]
            humidity = weather_data["main"]["humidity"]

            return {
                "temperature": temperature,
                "condition": condition,
                "humidity": humidity
            }
        else:
            return {"error": f"API Error: {response.status_code}"}

    except Exception as e:
        return {"error": f"Error: {str(e)}"}

def display_weather(lcd):
    """
    Display weather data on the LCD
    """
    global display_mode
    
    # Set display mode to weather
    display_mode = "weather"
    
    # Show loading message
    lcd.clear()
    lcd.message("Weather...")
    
    # Get weather data
    weather_data = get_etobicoke_weather()
    
    lcd.clear()
    if "error" in weather_data:
        lcd.message("Weather Error")
        lcd.message(weather_data["error"][:16], 2)  # Truncate to fit LCD width
    else:
        # Format and display weather information
        temp_str = f"Temp: {weather_data['temperature']:.1f}C"
        cond_str = f"{weather_data['condition']} {weather_data['humidity']}%"

        lcd.message(temp_str)
        lcd.message(cond_str, 2)
    
    # Set a timer to switch back to clock after 10 seconds
    global weather_timer
    if weather_timer:
        weather_timer.cancel()
    weather_timer = threading.Timer(10.0, switch_to_clock)
    weather_timer.start()

def switch_to_clock():
    """
    Switch display back to clock mode
    """
    global display_mode
    display_mode = "clock"
    # The main loop will update the display on the next iteration

def display_clock(lcd, force_update=False):
    """
    Display clock on the LCD
    """
    now = datetime.datetime.now()
    currenttime = now.strftime("%H:%M:%S")
    currentdate = now.strftime("%d-%m-%Y")
    
    # Only update if time/date changed or force update is requested
    if force_update or currenttime != display_clock.lasttime or currentdate != display_clock.lastdate:
        lcd.clear()
        lcd.message('%02d:%02d:%02d' % (now.hour, now.minute, now.second), 1)
        lcd.message('%02d-%02d-%04d' % (now.day, now.month, now.year), 2)
        display_clock.lasttime = currenttime
        display_clock.lastdate = currentdate

# Initialize static variables for display_clock function
display_clock.lasttime = ''
display_clock.lastdate = ''

def button_pressed():
    """Function called when button is pressed"""
    global display_mode
    if display_mode == "clock":
        display_weather(lcd)

# Assign callback function to button press event
button.when_pressed = button_pressed

# Display welcome message
lcd.message("Weather Clock")
lcd.message("Starting...", 2)
time.sleep(2)
lcd.clear()

# Main loop
try:
    while True:
        if display_mode == "clock":
            display_clock(lcd)
        time.sleep(0.1)
except KeyboardInterrupt:
    # Clean up on exit
    if weather_timer:
        weather_timer.cancel()
    lcd.clear()
    print("\nExiting program")