Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions Software/LMSourceCode/ImageProcessing/CameraTools/v4l2_trigger
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/bin/bash
#
# PiTrac External Trigger Utility for Mira220 (arducam-pivariety)
#
# Usage: v4l2_trigger <subdev_index> <mode>
# subdev_index: v4l-subdev index (e.g., 2 for /dev/v4l-subdev2 on Pi 5)
# mode: 0 = disable external trigger, 1 = enable external trigger
#
# Example: v4l2_trigger 2 1 # Enable external trigger on /dev/v4l-subdev2
#
# This uses the Arducam pivariety driver's v4l2 control 'trigger_mode':
# - trigger_mode=0: normal streaming (internal trigger)
# - trigger_mode=1: external trigger via FSIN pin, camera waits for signal
#

SUBDEV="${1:-2}"
MODE="${2:-1}"

v4l2-ctl -d "/dev/v4l-subdev${SUBDEV}" -c "trigger_mode=${MODE}"
exit $?
46 changes: 42 additions & 4 deletions Software/LMSourceCode/ImageProcessing/camera_hardware.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ namespace golf_sim {
{ "3", CameraModel::PiHQ },
{ "4", CameraModel::PiGS },
{ "5", CameraModel::InnoMakerIMX296GS_Mono },
{ "6", CameraModel::Mira220_Mono },
{ "100", CameraModel::kCameraUnknown },
};
if (camera_table.count(model_enum_value_string) == 0)
Expand Down Expand Up @@ -133,7 +134,7 @@ namespace golf_sim {


bool CameraHardware::camera_is_mono() const {
return (camera_model_ == CameraModel::InnoMakerIMX296GS_Mono);
return (camera_model_ == CameraModel::InnoMakerIMX296GS_Mono || camera_model_ == CameraModel::Mira220_Mono);
}


Expand Down Expand Up @@ -236,17 +237,17 @@ namespace golf_sim {
}

// This section deals with the common characteristics of some of the cameras
if (model == PiGS ||
if (model == PiGS ||
model == InnoMakerIMX296GS_Mono) {

GS_LOG_TRACE_MSG(trace, "Initializing with a PiGS or InnoMakerIMX296GS_Mono camera." );
GS_LOG_TRACE_MSG(trace, "Initializing with a PiGS or InnoMakerIMX296GS_Mono camera.");
// Sensor pixel width is 3.45uM square? No - 6.33mm diagonal. It appears that
// the actual width is the full resolution (1456) * 3.4uM = 4.95mm,
// Not simply the diagonal sensor width

sensor_width_ = (float)5.077365371; // 4.45; // (1456.0 * 3.4) / 1000; // = 4.95; // In mm 6.3 / sqrt(2.0); // TBD - Confirm math from diagonal measurement
sensor_height_ = (float)3.789078635; // 4.45; //(1088.0 * 3.4) / 1000; // In mm 6.3 / sqrt(2.0);

if (resolution_x_override_ > 0 && resolution_y_override_ > 0) {
resolution_x_ = resolution_x_override_;
resolution_y_ = resolution_y_override_;
Expand All @@ -263,6 +264,43 @@ namespace golf_sim {
video_resolution_y_ = 1080;

GS_LOG_TRACE_MSG(trace, "Video resolution (x,y) is: " + std::to_string(video_resolution_x_) + "/" + std::to_string(video_resolution_y_) + ".");
}

if (model == Mira220_Mono) {

// See https://look.ams-osram.com/m/7591efdbe4af32dc/original/Mira220-1-2-7-2-2-MP-NIR-enhanced-global-shutter-image-sensor.pdf for details

GS_LOG_TRACE_MSG(trace, "Initializing with a Mira220_Mono - based camera.");
// Sensor pixel width is 3.45uM square? No - 6.33mm diagonal. It appears that
// the actual width is the full resolution (1456) * 3.4uM = 4.95mm,
// Not simply the diagonal sensor width

sensor_width_ = (float)4.464; // 2.79um pixel size * 1600
sensor_height_ = (float)3.906; // 2.79um pixel size * 1400

if (resolution_x_override_ > 0 && resolution_y_override_ > 0) {
resolution_x_ = resolution_x_override_;
resolution_y_ = resolution_y_override_;
}
else {
// Defaults
resolution_x_ = 1600;
resolution_y_ = 1400;
}

// We ahve not tested .
video_resolution_x_ = resolution_x_;
video_resolution_y_ = resolution_y_;

GS_LOG_TRACE_MSG(trace, "Video resolution (x,y) is: " + std::to_string(video_resolution_x_) + "/" + std::to_string(video_resolution_y_) + ".");
}


// Ball radius parameters are common to most all of our potential cameras
if (model == PiGS ||
model == InnoMakerIMX296GS_Mono ||
model == Mira220_Mono) {


// Attempt to get the expected ball radius from the .json file
std::string ball_radius_pixels_at_40cm_name = "kExpectedBallRadiusPixelsAt40cmCamera" + std::string(camera_number == GsCameraNumber::kGsCamera1 ? "1" : "2");
Expand Down
1 change: 1 addition & 0 deletions Software/LMSourceCode/ImageProcessing/camera_hardware.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ namespace golf_sim {
PiHQ = 3,
PiGS = 4,
InnoMakerIMX296GS_Mono = 5,
Mira220_Mono = 6,
kCameraUnknown = 100
};

Expand Down
32 changes: 3 additions & 29 deletions Software/LMSourceCode/ImageProcessing/core/rpicam_app.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,35 +59,9 @@ static libcamera::PixelFormat mode_to_pixel_format(Mode const &mode)

static void set_pipeline_configuration(Platform platform)
{
GS_LOG_TRACE_MSG(trace, "Setting pipeline configuration file..");

// Respect any pre-existing value in the environment variable.
char const *existing_config = getenv("LIBCAMERA_RPI_CONFIG_FILE");
if (existing_config && existing_config[0]) {
GS_LOG_TRACE_MSG(trace, "LIBCAMERA_RPI_CONFIG_FILE already set.");
return;
}


// JPMOD - Added PISP options
// Otherwise point it at whichever of these we find first (if any) for the given platform.
static const std::vector<std::pair<Platform, std::string>> config_files = {
{ Platform::VC4, "/usr/local/share/libcamera/pipeline/rpi/vc4/rpi_apps.yaml" },
{ Platform::VC4, "/usr/share/libcamera/pipeline/rpi/vc4/rpi_apps.yaml" },
{ Platform::PISP, "/usr/local/share/libcamera/pipeline/rpi/pisp/rpi_apps.yaml" },
{ Platform::PISP, "/usr/share/libcamera/pipeline/rpi/pisp/rpi_apps.yaml" },
};

for (auto &config_file : config_files)
{
struct stat info;
if (config_file.first == platform && stat(config_file.second.c_str(), &info) == 0)
{
GS_LOG_TRACE_MSG(trace, "LIBCAMERA_RPI_CONFIG_FILE = " + config_file.second);
setenv("LIBCAMERA_RPI_CONFIG_FILE", config_file.second.c_str(), 1);
break;
}
}
// Let libcamera auto-detect the pipeline config — the hardcoded
// rpi_apps.yaml causes a segfault with Mira220 (Arducam pivariety).
(void)platform;
}

RPiCamApp::RPiCamApp(std::unique_ptr<Options> opts)
Expand Down
12 changes: 10 additions & 2 deletions Software/LMSourceCode/ImageProcessing/libcamera_interface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -982,7 +982,13 @@ bool SetLibcameraTuningFileEnvVariable(const GolfSimCamera& camera) {

std::string tuning_file;

if (camera.camera_hardware_.camera_is_mono()) {
// Mira220 (Arducam pivariety) — no matching tuning file exists.
// Let libcamera auto-detect the best fallback.
if (camera.camera_hardware_.camera_model_ == CameraHardware::CameraModel::Mira220_Mono) {
unsetenv("LIBCAMERA_RPI_TUNING_FILE");
return true;
}
else if (camera.camera_hardware_.camera_is_mono()) {
// If this is a mono camera, than we must use the "mono" tuning file, regardless of whether
// the camera is camera 1 or camera 2

Expand Down Expand Up @@ -1012,7 +1018,9 @@ bool SetLibcameraTuningFileEnvVariable(const GolfSimCamera& camera) {
}
}

setenv("LIBCAMERA_RPI_TUNING_FILE", tuning_file.c_str(), 1);
if (!tuning_file.empty()) {
setenv("LIBCAMERA_RPI_TUNING_FILE", tuning_file.c_str(), 1);
}

return true;
}
Expand Down
24 changes: 19 additions & 5 deletions Software/LMSourceCode/ImageProcessing/libcamera_jpeg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,22 @@ void SetExternalTrigger(bool& flag) {
return;
}
}

if (!flag && camera_model == gs::CameraHardware::CameraModel::Mira220_Mono) {

flag = true;

std::string trigger_mode_command = "$PITRAC_ROOT/ImageProcessing/CameraTools/v4l2_trigger 2 1";

GS_LOG_TRACE_MSG(trace, "ball_flight_camera_event_loop - Camera 2 trigger_mode_command = " + trigger_mode_command);
int command_result = system(trigger_mode_command.c_str());

if (command_result != 0) {
GS_LOG_TRACE_MSG(trace, "system(trigger_mode_command) failed.");
return;
}
}

}

// Run the triggered capture event loop on an already-opened camera.
Expand Down Expand Up @@ -178,11 +194,9 @@ bool cam2_run_event_loop(LibcameraJpegApp& app, cv::Mat& returnImg, bool send_pr
RPiCamApp::Msg msg = app.Wait();
if (msg.type == RPiCamApp::MsgType::Timeout)
{
GS_LOG_MSG(error, "ERROR: Device timeout detected, attempting a restart!!!");
app.StopCamera();
uint flags = RPiCamApp::FLAG_STILL_RGB;
app.ConfigureViewfinder(flags);
app.StartCamera();
// In external trigger mode, timeouts are expected while waiting
// for FSIN pulses. Just ignore and keep waiting for the trigger.
GS_LOG_TRACE_MSG(trace, "Device timeout (expected in external trigger mode) — continuing to wait.");
continue;
}

Expand Down
18 changes: 17 additions & 1 deletion Software/LMSourceCode/ImageProcessing/pulse_strobe.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,21 @@ namespace golf_sim {
void PulseStrobe::SendOnOffPulse(long length_us) {
#ifdef __unix__ // Ignore in Windows environment

if (kUsingActiveHighTriggerCamera) {
const CameraHardware::CameraModel camera_model = GolfSimCamera::kSystemSlot2CameraType;

// The Mira220 external trigger (trigger_mode=1) requires a longer
// HIGH pulse on FSIN than the IMX296. The Connector Board inverts
// GPIO→XTR, so we swap the on/off durations: the normally-short
// LOW pulse becomes a long LOW (XTR HIGH for ~66ms at 15fps),
// which Mira220 detects reliably.
if (camera_model == CameraHardware::CameraModel::Mira220_Mono) {
int kOnTimeUs = (int)((1.0 / kPrimingPulseFPS) * 1000000. - length_us);
lgGpioWrite(lggpio_chip_handle_, kPulseTriggerOutputPin, kON);
usleep(length_us);
lgGpioWrite(lggpio_chip_handle_, kPulseTriggerOutputPin, kOFF);
usleep(kOnTimeUs);
lgGpioWrite(lggpio_chip_handle_, kPulseTriggerOutputPin, kON);
} else if (kUsingActiveHighTriggerCamera) {
lgGpioWrite(lggpio_chip_handle_, kPulseTriggerOutputPin, kOFF);
usleep(length_us);
lgGpioWrite(lggpio_chip_handle_, kPulseTriggerOutputPin, kON);
Expand Down Expand Up @@ -649,6 +663,8 @@ namespace golf_sim {
}
}

// NOTE - We're not yet sure if a Mira220-based sensor will need any priming pulses. However, it should probably work similar to the IMX296

GS_LOG_TRACE_MSG(trace, "Sent " + std::to_string(kNumberPrimingPulses) + " initial priming pulses. About to pause for " +
std::to_string(kPauseBeforeReadyForFinalPrimingPulseMs) + " milliSeconds before sending penultimate priming pulse.");

Expand Down
17 changes: 15 additions & 2 deletions Software/web-server/camera_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,25 +28,29 @@ class CameraDetector:
"imx477": "Pi HQ Camera",
"imx296": "Global Shutter Camera",
"imx708": "Pi Camera v3",
"arducam-pivariety": "Arducam Mira220 (Mono)",
}

PITRAC_TYPES = {"ov5647": 1, "imx219": 2, "imx477": 3, "imx296": 4, "imx708": 0}
PITRAC_TYPES = {"ov5647": 1, "imx219": 2, "imx477": 3, "imx296": 4, "imx708": 0, "arducam-pivariety": 6}

CAMERA_STATUS = {
"ov5647": "DEPRECATED",
"imx219": "DEPRECATED",
"imx477": "DEPRECATED",
"imx296": "SUPPORTED",
"imx708": "UNSUPPORTED",
"arducam-pivariety": "EXPERIMENTAL",
}

PITRAC_TYPE_PI_GS = 4
PITRAC_TYPE_INNOMAKER = 5
PITRAC_TYPE_MIRA220 = 6

DT_ROOT = "/sys/firmware/devicetree/base"
DT_ROOT_ALT = "/proc/device-tree"

INNOMAKER_TRIGGER = "/usr/lib/pitrac/ImageProcessing/CameraTools/imx296_trigger"
MIRA220_TRIGGER = "/usr/lib/pitrac/ImageProcessing/CameraTools/v4l2_trigger"

def __init__(self):
self.pi_model = self._detect_pi_model()
Expand Down Expand Up @@ -194,7 +198,7 @@ def _parse_camera_info(self, output: str) -> List[Dict]:
"""Parse camera information from libcamera output"""
cameras = []

camera_pattern = r"^(\d+)\s*:\s*(\w+)\s*\[([^\]]+)\](?:\s*\(([^)]+)\))?"
camera_pattern = r"^(\d+)\s*:\s*([\w-]+)\s*\[([^\]]+)\](?:\s*\(([^)]+)\))?"

for match in re.finditer(camera_pattern, output, re.MULTILINE):
idx = int(match.group(1))
Expand Down Expand Up @@ -224,6 +228,9 @@ def _parse_camera_info(self, output: str) -> List[Dict]:
else:
pitrac_type = self.PITRAC_TYPE_PI_GS
description = "IMX296 (assumed color)"
elif sensor == "arducam-pivariety":
pitrac_type = self.PITRAC_TYPE_MIRA220
description = "Arducam Mira220 (Mono)"
else:
description = model_name

Expand Down Expand Up @@ -536,6 +543,12 @@ def get_camera_types(self) -> List[Dict]:
"description": "IMX296 Mono",
"status": "supported",
},
{
"value": 6,
"label": "Arducam Mira220",
"description": "Mira220 Mono Global Shutter",
"status": "experimental",
},
]

def get_lens_types(self) -> List[Dict]:
Expand Down
6 changes: 4 additions & 2 deletions Software/web-server/configurations.json
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,8 @@
"2": "Pi Camera v2 - IMX219 sensor (DEPRECATED)",
"3": "Pi HQ Camera - IMX477 sensor (DEPRECATED)",
"4": "Pi Global Shutter - IMX296 Color",
"5": "InnoMaker IMX296 - Mono sensor (RECOMMENDED)"
"5": "InnoMaker IMX296 - Mono sensor (RECOMMENDED)",
"6": "Osram Mira220 - Mono sensor (EXPERIMENTAL)"
},
"default": "5",
"requiresRestart": true,
Expand Down Expand Up @@ -391,7 +392,8 @@
"2": "Pi Camera v2 - IMX219 sensor (DEPRECATED)",
"3": "Pi HQ Camera - IMX477 sensor (DEPRECATED)",
"4": "Pi Global Shutter - IMX296 Color",
"5": "InnoMaker IMX296 - Mono sensor (RECOMMENDED)"
"5": "InnoMaker IMX296 - Mono sensor (RECOMMENDED)",
"6": "Osram Mira220 - Mono sensor (EXPERIMENTAL)"
},
"default": "5",
"requiresRestart": true,
Expand Down
2 changes: 1 addition & 1 deletion Software/web-server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
# Locked to defeat AE/AWB/tuning drift across calibration frames (sub-pixel
# corner bias). Tuning file matches production rcPi5GS.sh; shutter is shorter
# than production (11 vs 20 ms) to limit hand-tremor blur.
RPICAM_TUNING_FILE = "/usr/share/libcamera/ipa/rpi/pisp/imx296_noir.json"
RPICAM_TUNING_FILE = "/usr/share/libcamera/ipa/rpi/pisp/imx296_noir.json" # NOTE - Mira220 will need a different tuning file. For now, folks can just put the Mira220 file in this file
RPICAM_CAL_SHUTTER_US = 11000
RPICAM_CAL_GAIN = 1.3

Expand Down
10 changes: 10 additions & 0 deletions detect_pi_cameras.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ declare -A CAMERA_MODELS=(
["imx477"]="Pi HQ Camera"
["imx296"]="Global Shutter Camera"
["imx708"]="Pi Camera v3"
["arducam-pivariety"]="Arducam Mira220 (Mono)"
)

# PiTrac type mappings (from camera_hardware.h)
Expand All @@ -52,6 +53,7 @@ declare -A PITRAC_TYPES=(
["imx477"]=3 # Deprecated - not recommended
["imx296"]=4 # Default for IMX296 (Pi GS)
["imx708"]=0 # Unsupported
["arducam-pivariety"]=6 # Arducam Mira220 Mono
)

# Camera support status
Expand All @@ -61,11 +63,13 @@ declare -A CAMERA_STATUS=(
["imx477"]="DEPRECATED"
["imx296"]="SUPPORTED"
["imx708"]="UNSUPPORTED"
["arducam-pivariety"]="EXPERIMENTAL"
)

# IMX296 specific types
PITRAC_TYPE_PI_GS=4 # Raspberry Pi Global Shutter (color)
PITRAC_TYPE_INNOMAKER=5 # InnoMaker IMX296 (mono)
PITRAC_TYPE_MIRA220=6 # Arducam Mira220 Mono

# Device tree root paths
DT_ROOT="/sys/firmware/devicetree/base"
Expand Down Expand Up @@ -440,6 +444,12 @@ detect_cameras() {
fi
fi

# Special handling for Arducam Mira220 (arducam-pivariety driver)
if [[ "$sensor" == "arducam-pivariety" ]]; then
pitrac_type=$PITRAC_TYPE_MIRA220
description="Arducam Mira220 (Mono)"
fi

CAMERA_PITRAC_TYPE["$idx"]="$pitrac_type"
CAMERA_DESCRIPTION["$idx"]="$description"

Expand Down
Loading
Loading