413 lines
14 KiB
Vala
413 lines
14 KiB
Vala
// Camera client for Olympus OPC protocol
|
|
// Communicates with camera at 192.168.0.10:80 via HTTP
|
|
|
|
public class ImageInfo : Object {
|
|
public string directory { get; set; }
|
|
public string filename { get; set; }
|
|
public int64 size { get; set; }
|
|
public int attribute { get; set; }
|
|
public DateTime? date_time { get; set; }
|
|
public string full_path { get; set; }
|
|
|
|
public bool is_hidden {
|
|
get { return (attribute & 2) != 0; }
|
|
}
|
|
|
|
public bool is_directory {
|
|
get { return (attribute & 16) != 0; }
|
|
}
|
|
|
|
public string display_date {
|
|
owned get {
|
|
if (date_time == null) return "Unknown";
|
|
return date_time.format ("%Y-%m-%d %H:%M");
|
|
}
|
|
}
|
|
|
|
public ImageInfo (string directory, string filename, int64 size, int attribute,
|
|
DateTime? date_time, string full_path) {
|
|
this.directory = directory;
|
|
this.filename = filename;
|
|
this.size = size;
|
|
this.attribute = attribute;
|
|
this.date_time = date_time;
|
|
this.full_path = full_path;
|
|
}
|
|
}
|
|
|
|
public class CameraClient : Object {
|
|
private Soup.Session session;
|
|
private MainContext? context = null;
|
|
private bool owns_context = false;
|
|
private const string CAMERA_HOST = "192.168.0.10";
|
|
private const int CAMERA_PORT = 80;
|
|
private const int TIMEOUT_SECONDS = 5;
|
|
|
|
public bool connected { get; private set; default = false; }
|
|
public static bool debug { get; set; default = false; }
|
|
|
|
public CameraClient () {
|
|
// Worker threads typically have no thread-default GMainContext.
|
|
// Soup.Session.send_message internally iterates a GMainContext,
|
|
// so we must push one to avoid crashes with NULL context.
|
|
context = MainContext.get_thread_default ();
|
|
if (context == null) {
|
|
context = new MainContext ();
|
|
context.push_thread_default ();
|
|
owns_context = true;
|
|
}
|
|
session = new Soup.Session ();
|
|
session.user_agent = "OI.Share v2";
|
|
session.timeout = TIMEOUT_SECONDS;
|
|
}
|
|
|
|
~CameraClient () {
|
|
if (owns_context && context != null) {
|
|
context.pop_thread_default ();
|
|
}
|
|
}
|
|
|
|
private string build_url (string path) {
|
|
return "http://%s:%d%s".printf( CAMERA_HOST, CAMERA_PORT, path);
|
|
}
|
|
|
|
// Perform synchronous HTTP GET, return response body as Bytes
|
|
private Bytes do_get (string url) throws Error {
|
|
if (debug) {
|
|
stdout.printf (">>> GET %s\n", url);
|
|
}
|
|
|
|
var msg = new Soup.Message ("GET", url);
|
|
session.send_message (msg);
|
|
|
|
if (debug) {
|
|
stdout.printf ("<<< HTTP %u %s\n", msg.status_code, msg.reason_phrase);
|
|
}
|
|
|
|
if (msg.status_code != 200) {
|
|
if (debug) {
|
|
stdout.printf ("<<< ERROR: HTTP %u - body follows:\n", msg.status_code);
|
|
unowned uint8[] err_raw = msg.response_body.data;
|
|
if (err_raw != null) {
|
|
var err_body = (string) err_raw;
|
|
if (((string) err_body).validate ())
|
|
stdout.printf ("%s\n", (string) err_body);
|
|
}
|
|
}
|
|
throw new IOError.FAILED (
|
|
"HTTP request to %s failed with status %u", url, msg.status_code
|
|
);
|
|
}
|
|
|
|
// Extract response body as Bytes.
|
|
// Use unowned access to avoid an extra intermediate copy.
|
|
unowned uint8[] raw_data = msg.response_body.data;
|
|
var bytes = new Bytes (raw_data);
|
|
|
|
if (debug) {
|
|
if (raw_data != null) {
|
|
var body_str = (string) raw_data;
|
|
if (((string) body_str).validate ()) {
|
|
stdout.printf ("<<< Body (%ld bytes):\n%s\n", raw_data.length, (string) body_str);
|
|
} else {
|
|
stdout.printf ("<<< Body: %ld bytes (binary data)\n", raw_data.length);
|
|
}
|
|
} else {
|
|
stdout.printf ("<<< Body: 0 bytes (empty)\n");
|
|
}
|
|
}
|
|
|
|
return bytes;
|
|
}
|
|
|
|
// --- Public API ---
|
|
|
|
// Probe camera by fetching its command list.
|
|
// Modern Olympus cameras don't necessarily return OPC mode from
|
|
// get_connectmode.cgi; instead they respond to get_commandlist.cgi
|
|
// directly. This matches the approach used by the working
|
|
// olympus-wifi library (OI.Share v2 user-agent).
|
|
public bool check_connection( ) throws Error {
|
|
if (debug) {
|
|
stdout.printf ("[debug] Probing camera with get_commandlist.cgi…\n");
|
|
}
|
|
try {
|
|
var body = do_get (build_url ("/get_commandlist.cgi"));
|
|
string xml = (string)body.get_data ();
|
|
if (xml.contains ("<?xml") && xml.contains( "<cgi")) {
|
|
connected = true;
|
|
if (debug) {
|
|
stdout.printf ("[debug] Camera responded with valid command list, connected.\n");
|
|
}
|
|
return true;
|
|
}
|
|
if (debug) {
|
|
stdout.printf ("[debug] Unexpected response from get_commandlist:\n%s\n", xml);
|
|
}
|
|
} catch (Error e) {
|
|
if (debug) {
|
|
stdout.printf ("[debug] get_commandlist failed: %s\n", e.message);
|
|
}
|
|
}
|
|
|
|
// Fallback: try get_connectmode.cgi (older cameras)
|
|
if (debug) {
|
|
stdout.printf ("[debug] Falling back to get_connectmode.cgi…\n");
|
|
}
|
|
try {
|
|
var body = do_get (build_url ("/get_connectmode.cgi"));
|
|
string xml = (string)body.get_data ();
|
|
if (xml.contains ("<connectmode>OPC</connectmode>")) {
|
|
connected = true;
|
|
return true;
|
|
}
|
|
if (debug) {
|
|
stdout.printf ("[debug] get_connectmode returned: %s\n", xml.replace( "\n", " ").strip( ));
|
|
}
|
|
} catch (Error e) {
|
|
if (debug) {
|
|
stdout.printf ("[debug] get_connectmode failed: %s\n", e.message);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Switch operation mode (play, rec)
|
|
// Note: modern Olympus cameras use "switch_cammode.cgi" (one 'm')
|
|
// not "switch_cameramode.cgi" as in the older OPC spec.
|
|
public bool switch_mode( string mode) throws Error {
|
|
string url = build_url ("/switch_cammode.cgi?mode=%s".printf( mode));
|
|
var body = do_get (url);
|
|
string xml = (string)body.get_data ();
|
|
if (debug) {
|
|
stdout.printf ("[debug] switch_cammode response: %s\n", xml.replace( "\n", " ").strip( ));
|
|
}
|
|
// Some cameras return <result>OK, some just return HTTP 200 with empty body
|
|
return true;
|
|
}
|
|
|
|
// Activate session (some cameras require this before other commands)
|
|
public void activate( ) throws Error {
|
|
try {
|
|
do_get (build_url ("/get_activate.cgi"));
|
|
if (debug) {
|
|
stdout.printf ("[debug] get_activate succeeded\n");
|
|
}
|
|
} catch (Error e) {
|
|
if (debug) {
|
|
stdout.printf ("[debug] get_activate failed (non-fatal): %s\n", e.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get list of items in a DCF directory
|
|
// Returns an array of ImageInfo
|
|
// Note: 404 means empty directory on modern cameras, returns empty list.
|
|
public ImageInfo[] get_image_list( string dir) throws Error {
|
|
string url = build_url ("/get_imglist.cgi?DIR=%s".printf( dir));
|
|
Bytes body;
|
|
try {
|
|
body = do_get (url);
|
|
} catch (Error e) {
|
|
if (e.message.contains ("status 404")) {
|
|
if (debug) {
|
|
stdout.printf ("[debug] get_imglist(%s): 404 (empty directory)\n", dir);
|
|
}
|
|
return new ImageInfo[0];
|
|
}
|
|
throw e;
|
|
}
|
|
string text = (string)body.get_data ();
|
|
|
|
string[] lines = text.split ("\n");
|
|
|
|
if (debug) {
|
|
stdout.printf ("[debug] get_imglist(%s) response (%lu bytes):\n", dir, body.get_size( ));
|
|
// Print first few lines for debugging
|
|
for (int i = 0; i < lines.length && i < 5; i++) {
|
|
stdout.printf ("[debug] line %d: %s\n", i, lines[i]);
|
|
}
|
|
}
|
|
var results = new ImageInfo[lines.length - 1]; // max possible
|
|
int count = 0;
|
|
|
|
for (int i = 0; i < lines.length; i++) {
|
|
string line = lines[i].strip ();
|
|
if (line.length == 0 || line == "VER_100") continue;
|
|
|
|
string[] parts = line.split (",", 6);
|
|
if (parts.length < 6) continue;
|
|
|
|
string directory = parts[0];
|
|
string filename = parts[1];
|
|
int64 size = int64.parse (parts[2]);
|
|
int attribute = int.parse (parts[3]);
|
|
string full_path = "%s/%s".printf( directory, filename);
|
|
|
|
// Parse packed date
|
|
int64 packed_date = int64.parse( parts[4]);
|
|
int64 packed_time = int64.parse (parts[5]);
|
|
|
|
DateTime? dt = decode_opc_date (packed_date, packed_time);
|
|
|
|
results[count] = new ImageInfo (directory, filename, size, attribute, dt, full_path);
|
|
count++;
|
|
}
|
|
|
|
// Trim to actual count
|
|
var trimmed = new ImageInfo[count];
|
|
for (int i = 0; i < count; i++) {
|
|
trimmed[i] = results[i];
|
|
}
|
|
return trimmed;
|
|
}
|
|
|
|
// Discover all DCF directories on the camera.
|
|
// Probes multiple paths and tries common DCF directory names.
|
|
// Returns an array of directory paths that contain images.
|
|
public string[] discover_dcf_directories( ) throws Error {
|
|
string[] found_dirs = new string[0];
|
|
|
|
// Probe known DCF paths that could contain subdirectories
|
|
string[] probe_paths = {"/", "/DCIM/"};
|
|
|
|
int count = 0;
|
|
foreach (string probe in probe_paths) {
|
|
try {
|
|
var items = get_image_list (probe);
|
|
// Make a first pass to count subdirectories
|
|
int sub_count = 0;
|
|
foreach (var item in items) {
|
|
if (item.is_directory && !item.is_hidden) {
|
|
sub_count++;
|
|
}
|
|
}
|
|
if (sub_count > 0) {
|
|
found_dirs = new string[sub_count];
|
|
int idx = 0;
|
|
foreach (var item in items) {
|
|
if (item.is_directory && !item.is_hidden) {
|
|
found_dirs[idx] = item.full_path;
|
|
idx++;
|
|
if (debug) {
|
|
stdout.printf ("[debug] Discovered DCF dir: %s\n", item.full_path);
|
|
}
|
|
}
|
|
}
|
|
count = sub_count;
|
|
break;
|
|
}
|
|
} catch (Error e) {
|
|
if (debug) {
|
|
stdout.printf ("[debug] Probe %s: %s\n", probe, e.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
// If probe paths yielded directories, return them
|
|
if (count > 0) {
|
|
return found_dirs;
|
|
}
|
|
|
|
// Fallback: try standard DCF directory names directly
|
|
string[] fallbacks = {"/DCIM/100OLYMP", "/DCIM/101OLYMP", "/DCIM/102OLYMP",
|
|
"/DCIM/103OLYMP", "/DCIM/104OLYMP", "/DCIM/105OLYMP"};
|
|
int fb_count = 0;
|
|
foreach (string d in fallbacks) {
|
|
try {
|
|
get_image_list (d);
|
|
fb_count++;
|
|
} catch (Error e) {
|
|
// doesn't exist, skip
|
|
}
|
|
}
|
|
if (fb_count > 0) {
|
|
found_dirs = new string[fb_count];
|
|
int idx = 0;
|
|
foreach (string d in fallbacks) {
|
|
try {
|
|
get_image_list (d);
|
|
found_dirs[idx] = d;
|
|
idx++;
|
|
if (debug) {
|
|
stdout.printf ("[debug] Found DCF directory (fallback): %s\n", d);
|
|
}
|
|
} catch (Error e) {
|
|
}
|
|
}
|
|
return found_dirs;
|
|
}
|
|
|
|
if (debug) {
|
|
stdout.printf ("[debug] No DCF directories found - camera may have no SD card\n");
|
|
}
|
|
|
|
return found_dirs;
|
|
}
|
|
|
|
// Get all images across all DCF directories
|
|
public ImageInfo[] get_all_images( ) throws Error {
|
|
var dirs = discover_dcf_directories ();
|
|
|
|
// First pass: count total images
|
|
int total = 0;
|
|
foreach (string dir in dirs) {
|
|
var images = get_image_list (dir);
|
|
foreach (var img in images) {
|
|
if (!img.is_directory && !img.is_hidden) {
|
|
total++;
|
|
}
|
|
}
|
|
}
|
|
|
|
var all = new ImageInfo[total];
|
|
int idx = 0;
|
|
foreach (string dir in dirs) {
|
|
var images = get_image_list (dir);
|
|
foreach (var img in images) {
|
|
if (!img.is_directory && !img.is_hidden) {
|
|
all[idx] = img;
|
|
idx++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return all;
|
|
}
|
|
|
|
// Get thumbnail image (160x120 JPEG)
|
|
public Bytes get_thumbnail( string path) throws Error {
|
|
string url = build_url ("/get_thumbnail.cgi?DIR=%s".printf( path));
|
|
return do_get (url);
|
|
}
|
|
|
|
// Get image for control device display (1920x1440 max)
|
|
public Bytes get_screennail( string path) throws Error {
|
|
string url = build_url ("/get_screennail.cgi?DIR=%s".printf( path));
|
|
return do_get (url);
|
|
}
|
|
|
|
// Download original image file from camera
|
|
public Bytes download_original( string path) throws Error {
|
|
return do_get (build_url (path));
|
|
}
|
|
}
|
|
|
|
// Helper to decode packed date integer from OPC protocol
|
|
// bits 4-0: day, bits 8-5: month, bits 15-9: year (offset from 1980)
|
|
DateTime? decode_opc_date( int64 packed_date, int64 packed_time) {
|
|
int day = (int)(packed_date & 0x1F);
|
|
int month = (int)((packed_date >> 5) & 0x0F);
|
|
int year = (int)((packed_date >> 9) + 1980);
|
|
int seconds = (int)(packed_time & 0x1F) * 2;
|
|
int minutes = (int)((packed_time >> 5) & 0x3F);
|
|
int hours = (int)((packed_time >> 11) & 0x1F);
|
|
|
|
if (year < 1980 || month < 1 || month > 12 || day < 1 || day > 31) {
|
|
return null;
|
|
}
|
|
|
|
return new DateTime.local (year, month, day, hours, minutes, seconds);
|
|
}
|