diff --git a/CHANGELOG.md b/CHANGELOG.md index f9b6bea..467545c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -123,3 +123,19 @@ Next: - added direct exercise creation from the in-session exercise chooser; - kept the final rolling 12-month `MM/YY` dashboard label inside its frame. + + +### Direct Android USB/MTP transport foundation + +- added `libudev` discovery of physical MTP devices; +- filtered MTP USB interface children to avoid duplicate phones; +- added exact bus/device matching into `libmtp`; +- added MTP storage enumeration; +- added root-folder discovery/creation; +- added direct MTP text-file upload; +- added direct folder-child listing; +- added direct MTP file download; +- split cached and uncached libmtp open paths where required; +- validated a real Android internal-storage write/list/read roundtrip; +- kept Android access mount-free: no GVFS/FUSE mount lifecycle is required. + diff --git a/docs/android.md b/docs/android.md index 8eba8e5..91e4a6c 100644 --- a/docs/android.md +++ b/docs/android.md @@ -205,3 +205,32 @@ A valid Android export can still conflict with an independently edited TUI catal The TUI owns final reconciliation. Android must not assume that a matching display name means two different IDs may be silently merged. + + +## 15. USB file-transfer transport + +The initial Android/Linux integration uses standard Android **file transfer +(MTP)** mode. + +The Linux TUI/core accesses the device directly with `libudev` + `libmtp`. +Trainlog does not require a mounted Android filesystem. + +Validated Linux-side capabilities: + +```text +USB_MTP_DETECTION=PASS +MTP_STORAGE_ACCESS=PASS +MTP_WRITE=PASS +MTP_READ=PASS +MTP_ROUNDTRIP=PASS +``` + +The transport foundation currently uses a root `Trainlog` folder for physical +validation. + +The final JSON exchange subdirectory/naming convention is defined in the next +slice before the Android recorder depends on it. + +The Android application itself will only need to produce a valid frozen +Trainlog JSON v1 document and place it in the agreed exchange area. + diff --git a/docs/architecture.md b/docs/architecture.md index 7d685b2..4607e4a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -138,3 +138,35 @@ Examples: - duplicate session: report already imported, do not duplicate; - unknown exercise: import it when valid catalog data is present; - incomplete active session: preserve it explicitly rather than silently inventing an end time. + + +## 7. Direct Android USB/MTP transport + +The Linux side does not require the Android device to be mounted as a normal +filesystem. + +Transport layering is: + +```text +Android USB file-transfer mode + | + v +libudev physical-device discovery + | + | bus number + device number + v +libmtp exact raw-device open + | + v +Android internal MTP storage +``` + +This avoids GVFS/FUSE mount state and manual mount/unmount lifecycle management. + +`libudev` owns physical-device discovery. `libmtp` owns storage and object +operations. The JSON exchange layer remains above both and stays independent +from USB/MTP backend details. + +Current transport foundation supports folder creation, file upload, folder +listing, file download, and verified byte-for-byte roundtrip. + diff --git a/docs/reviews/mtp_transport.md b/docs/reviews/mtp_transport.md new file mode 100644 index 0000000..c50ba56 --- /dev/null +++ b/docs/reviews/mtp_transport.md @@ -0,0 +1,112 @@ +# Direct USB/MTP transport checkpoint + +## Status + +```text +USB_MTP_DETECTION=PASS +MTP_STORAGE_ACCESS=PASS +MTP_ROOT_FOLDER_ACCESS=PASS +MTP_WRITE=PASS +MTP_LIST_FOLDER=PASS +MTP_READ=PASS +MTP_ROUNDTRIP=PASS +MTP_TRANSPORT_FOUNDATION=PASS +JSON_V1_MTP_TRANSFER=NEXT +TRAINLOG_FORMAT_V1=FROZEN +``` + +Trainlog now accesses Android file-transfer mode directly through Linux USB/MTP. +No GVFS mount, FUSE mount, file-manager mount, or manual mount/unmount cycle is +required. + +## Detection + +The discovery layer uses `libudev`. + +Only physical USB devices with: + +```text +DEVTYPE=usb_device +ID_MTP_DEVICE=1 +``` + +are returned. + +USB interface children such as `6-1:1.0` are deliberately ignored so one +physical phone is never reported several times. + +The device record contains: + +- USB bus number; +- USB device number; +- vendor/product IDs; +- vendor/model strings when available; +- serial number when available; +- sysfs path. + +## MTP access + +The transport layer uses `libmtp`. + +The udev bus/device pair is matched against libmtp raw devices so Trainlog opens +the exact physical phone selected by discovery. + +Implemented operations: + +- enumerate MTP storage areas; +- expose capacity and free-space information; +- find or create a root folder; +- upload a local text file; +- list direct children of one MTP folder; +- download one MTP file to a local path. + +Cached and uncached libmtp opening modes are kept separate because folder-tree +operations and direct `LIBMTP_Get_Files_And_Folders()` traversal have different +backend requirements. + +## Physical validation + +The checkpoint was validated against an Android Samsung device in USB +"file transfer" / MTP mode. + +Observed storage: + +```text +description = Stockage interne +storage_id = 0x00010001 +access = read/write +``` + +Trainlog created: + +```text +Stockage interne/ +└── Trainlog/ + └── trainlog-probe.txt +``` + +The probe was then listed, downloaded back to the Linux host, and compared +byte-for-byte with the original local content. + +Final result: + +```text +ROUNDTRIP=PASS Trainlog/trainlog-probe.txt +``` + +## Boundary + +This checkpoint validates the transport foundation only. + +It does **not** yet claim that a real Trainlog JSON v1 session has been +transferred or imported. + +Next: + +```text +1. define the Trainlog MTP exchange directory layout +2. transfer a real valid JSON v1 document +3. verify byte-for-byte readback +4. feed the downloaded document to the frozen v1 validator/import path +5. build the minimal Android recorder on top of that contract +``` diff --git a/docs/roadmap.md b/docs/roadmap.md index 02aff9b..f362f54 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -198,3 +198,35 @@ ANDROID_MINIMAL_RECORDER=AFTER JSON_V1_USB_IMPORT_EXPORT=AFTER ``` + + +## Direct MTP transport checkpoint + +```text +USB_MTP_DETECTION=PASS +MTP_STORAGE_ACCESS=PASS +MTP_ROOT_FOLDER_ACCESS=PASS +MTP_WRITE=PASS +MTP_LIST_FOLDER=PASS +MTP_READ=PASS +MTP_ROUNDTRIP=PASS +MTP_TRANSPORT_FOUNDATION=PASS +JSON_V1_MTP_TRANSFER=NEXT +ANDROID_MINIMAL_RECORDER=AFTER +TRAINLOG_FORMAT_V1=FROZEN +``` + +The Linux side now detects one physical MTP phone without counting USB +interface children, opens the exact device with libmtp, accesses internal +storage, and performs a verified write/list/read roundtrip without mounting the +phone. + +Next implementation slice: + +```text +1. freeze the MTP exchange directory/file convention +2. roundtrip a real examples/session-v1.json +3. validate/import the downloaded JSON +4. begin the minimal Android recorder +``` + diff --git a/docs/tests.md b/docs/tests.md index ef6b4b1..496c02f 100644 --- a/docs/tests.md +++ b/docs/tests.md @@ -153,3 +153,40 @@ python tools/validate_json.py python tools/validate_import_contract.py git diff --check ``` + + +## 11. USB/MTP transport validation + +The compiled suite now contains: + +```text +usb +mtp +``` + +The `usb` test covers API validation, physical-device enumeration invariants, +and rejection of duplicated USB interface children. + +The `mtp` test covers bounded API argument validation for storage, folder, +upload, listing, and download entry points. + +Hardware probes additionally validate the real device path: + +```text +trainlog-usb-probe +trainlog-mtp-probe +trainlog-mtp-exchange-probe +trainlog-mtp-roundtrip-probe +``` + +Physical checkpoint result: + +```text +MTP devices: 1 +ROUNDTRIP=PASS Trainlog/trainlog-probe.txt +``` + +The hardware probe is intentionally separate from the normal automated test +suite because CI is not expected to have a connected unlocked Android MTP +device. + diff --git a/tui/include/trainlog/mtp.h b/tui/include/trainlog/mtp.h new file mode 100644 index 0000000..5ea88b5 --- /dev/null +++ b/tui/include/trainlog/mtp.h @@ -0,0 +1,107 @@ +#ifndef TRAINLOG_MTP_H +#define TRAINLOG_MTP_H + +/** + * @file mtp.h + * @brief MTP device access and storage enumeration. + */ + +#include +#include +#include + +#include "trainlog/status.h" + +#define TRAINLOG_MTP_STORAGE_TEXT_MAX 255U + +typedef struct TrainlogMtpStorage { + uint32_t storage_id; + uint16_t storage_type; + uint16_t filesystem_type; + uint16_t access_capability; + uint64_t max_capacity_bytes; + uint64_t free_space_bytes; + uint64_t free_space_objects; + char description[TRAINLOG_MTP_STORAGE_TEXT_MAX + 1U]; + char volume_identifier[TRAINLOG_MTP_STORAGE_TEXT_MAX + 1U]; +} TrainlogMtpStorage; + +/** + * @brief Open one exact MTP device and list its storage areas. + * + * bus_number/device_number come from TrainlogUsbDevice. + * + * Passing output=NULL with capacity=0 is a count-only query. + * Insufficient output capacity is rejected rather than silently truncated. + */ +TrainlogStatus trainlog_mtp_list_storages( + unsigned int bus_number, + unsigned int device_number, + TrainlogMtpStorage *output, + size_t capacity, + size_t *output_count +); + +/** + * @brief Find or create one root folder on an exact MTP storage. + */ +TrainlogStatus trainlog_mtp_ensure_root_folder( + unsigned int bus_number, + unsigned int device_number, + uint32_t storage_id, + const char *folder_name, + uint32_t *output_folder_id, + bool *output_created +); + +/** + * @brief Upload one local text file into an exact MTP folder. + */ +TrainlogStatus trainlog_mtp_send_text_file( + unsigned int bus_number, + unsigned int device_number, + uint32_t storage_id, + uint32_t parent_folder_id, + const char *local_path, + const char *remote_filename, + uint32_t *output_item_id +); + +#define TRAINLOG_MTP_ENTRY_NAME_MAX 255U + +typedef struct TrainlogMtpEntry { + uint32_t item_id; + uint32_t parent_id; + uint32_t storage_id; + uint64_t size_bytes; + bool folder; + char name[TRAINLOG_MTP_ENTRY_NAME_MAX + 1U]; +} TrainlogMtpEntry; + +/** + * @brief List direct children of one MTP folder. + * + * Passing output=NULL with capacity=0 is a count-only query. + * Insufficient capacity is rejected rather than silently truncated. + */ +TrainlogStatus trainlog_mtp_list_folder( + unsigned int bus_number, + unsigned int device_number, + uint32_t storage_id, + uint32_t parent_folder_id, + TrainlogMtpEntry *output, + size_t capacity, + size_t *output_count +); + +/** + * @brief Download one MTP object to a local file. + */ +TrainlogStatus trainlog_mtp_receive_file( + unsigned int bus_number, + unsigned int device_number, + uint32_t item_id, + const char *local_path +); + +#endif diff --git a/tui/include/trainlog/usb.h b/tui/include/trainlog/usb.h new file mode 100644 index 0000000..b4fed58 --- /dev/null +++ b/tui/include/trainlog/usb.h @@ -0,0 +1,32 @@ +#ifndef TRAINLOG_USB_H +#define TRAINLOG_USB_H + +#include +#include + +#include "trainlog/status.h" + +#define TRAINLOG_USB_VENDOR_MAX 127U +#define TRAINLOG_USB_MODEL_MAX 127U +#define TRAINLOG_USB_SERIAL_MAX 255U +#define TRAINLOG_USB_SYSPATH_MAX 511U + +typedef struct TrainlogUsbDevice { + unsigned int bus_number; + unsigned int device_number; + unsigned int vendor_id; + unsigned int product_id; + bool mtp; + char vendor[TRAINLOG_USB_VENDOR_MAX + 1U]; + char model[TRAINLOG_USB_MODEL_MAX + 1U]; + char serial[TRAINLOG_USB_SERIAL_MAX + 1U]; + char syspath[TRAINLOG_USB_SYSPATH_MAX + 1U]; +} TrainlogUsbDevice; + +TrainlogStatus trainlog_usb_list_mtp_devices( + TrainlogUsbDevice *output, + size_t capacity, + size_t *output_count +); + +#endif diff --git a/tui/meson.build b/tui/meson.build index 54e9565..7507a5f 100644 --- a/tui/meson.build +++ b/tui/meson.build @@ -2,6 +2,8 @@ cc = meson.get_compiler('c') sqlite3_dep = dependency('sqlite3', required: true) uuid_dep = dependency('uuid', required: true) +udev_dep = dependency('libudev', required: true) +mtp_dep = dependency('libmtp', required: true) ncursesw_dep = dependency('ncursesw', required: false) if not ncursesw_dep.found() @@ -31,6 +33,8 @@ trainlog_core_sources = files( 'src/duration.c', 'src/id.c', 'src/timeutil.c', + 'src/usb.c', + 'src/mtp.c', ) trainlog_core = static_library( @@ -42,6 +46,8 @@ trainlog_core = static_library( uuid_dep, utf8proc_dep, m_dep, + udev_dep, + mtp_dep, ], c_args: strict_c_args, ) @@ -54,6 +60,8 @@ trainlog_core_dep = declare_dependency( uuid_dep, utf8proc_dep, m_dep, + udev_dep, + mtp_dep, ], ) @@ -183,3 +191,55 @@ test( 'body_observation_edit', test_body_observation_edit, ) + +trainlog_usb_probe = executable( + 'trainlog-usb-probe', + 'tools/usb_probe.c', + dependencies: trainlog_core_dep, + c_args: strict_c_args, +) + +test_usb = executable( + 'test_usb', + 'tests/test_usb.c', + dependencies: trainlog_core_dep, + c_args: strict_c_args, +) + +test( + 'usb', + test_usb, +) + +trainlog_mtp_probe = executable( + 'trainlog-mtp-probe', + 'tools/mtp_probe.c', + dependencies: trainlog_core_dep, + c_args: strict_c_args, +) + +test_mtp = executable( + 'test_mtp', + 'tests/test_mtp.c', + dependencies: trainlog_core_dep, + c_args: strict_c_args, +) + +test( + 'mtp', + test_mtp, +) + +trainlog_mtp_exchange_probe = executable( + 'trainlog-mtp-exchange-probe', + 'tools/mtp_exchange_probe.c', + dependencies: trainlog_core_dep, + c_args: strict_c_args, +) + +trainlog_mtp_roundtrip_probe = executable( + 'trainlog-mtp-roundtrip-probe', + 'tools/mtp_roundtrip_probe.c', + dependencies: trainlog_core_dep, + c_args: strict_c_args, +) diff --git a/tui/src/mtp.c b/tui/src/mtp.c new file mode 100644 index 0000000..36ef336 --- /dev/null +++ b/tui/src/mtp.c @@ -0,0 +1,779 @@ +/** + * @file mtp.c + * @brief libmtp-backed storage access. + */ + +#include "trainlog/mtp.h" + +#include +#include +#include +#include +#include +#include + +#include + +static void mtp_copy_text( + char *output, + size_t capacity, + const char *value +) +{ + if (output == NULL || + capacity == 0U) { + return; + } + + if (value == NULL) { + output[0] = '\0'; + return; + } + + (void)snprintf( + output, + capacity, + "%s", + value + ); +} + +static bool mtp_raw_device_matches( + const LIBMTP_raw_device_t *device, + unsigned int bus_number, + unsigned int device_number +) +{ + if (device == NULL || + device_number > UINT8_MAX) { + return false; + } + + return + device->bus_location == + (uint32_t)bus_number && + device->devnum == + (uint8_t)device_number; +} + +static TrainlogStatus mtp_detect_raw_devices( + LIBMTP_raw_device_t **output_devices, + int *output_count +) +{ + LIBMTP_error_number_t error; + + if (output_devices == NULL || + output_count == NULL) { + return + TRAINLOG_STATUS_INVALID_ARGUMENT; + } + + *output_devices = NULL; + *output_count = 0; + + LIBMTP_Init(); + + error = + LIBMTP_Detect_Raw_Devices( + output_devices, + output_count + ); + + switch (error) { + case LIBMTP_ERROR_NONE: + return TRAINLOG_STATUS_OK; + + case LIBMTP_ERROR_NO_DEVICE_ATTACHED: + return TRAINLOG_STATUS_NOT_FOUND; + + case LIBMTP_ERROR_MEMORY_ALLOCATION: + return TRAINLOG_STATUS_SYSTEM_ERROR; + + case LIBMTP_ERROR_GENERAL: + case LIBMTP_ERROR_PTP_LAYER: + case LIBMTP_ERROR_USB_LAYER: + case LIBMTP_ERROR_STORAGE_FULL: + case LIBMTP_ERROR_CONNECTING: + case LIBMTP_ERROR_CANCELLED: + default: + return TRAINLOG_STATUS_SYSTEM_ERROR; + } +} + +TrainlogStatus trainlog_mtp_list_storages( + unsigned int bus_number, + unsigned int device_number, + TrainlogMtpStorage *output, + size_t capacity, + size_t *output_count +) +{ + LIBMTP_raw_device_t *raw_devices = NULL; + LIBMTP_mtpdevice_t *device = NULL; + LIBMTP_devicestorage_t *storage; + TrainlogStatus status; + int raw_count = 0; + int index; + size_t discovered = 0U; + + if (output_count == NULL || + (output == NULL && + capacity != 0U) || + device_number > UINT8_MAX) { + return + TRAINLOG_STATUS_INVALID_ARGUMENT; + } + + *output_count = 0U; + + status = + mtp_detect_raw_devices( + &raw_devices, + &raw_count + ); + + if (status != TRAINLOG_STATUS_OK) { + free(raw_devices); + return status; + } + + for (index = 0; + index < raw_count; + ++index) { + if (mtp_raw_device_matches( + &raw_devices[index], + bus_number, + device_number + )) { + device = + LIBMTP_Open_Raw_Device( + &raw_devices[index] + ); + + break; + } + } + + if (device == NULL) { + free(raw_devices); + return TRAINLOG_STATUS_NOT_FOUND; + } + + if (LIBMTP_Get_Storage( + device, + LIBMTP_STORAGE_SORTBY_NOTSORTED + ) != 0) { + LIBMTP_Clear_Errorstack(device); + LIBMTP_Release_Device(device); + free(raw_devices); + + return + TRAINLOG_STATUS_SYSTEM_ERROR; + } + + storage = device->storage; + + while (storage != NULL) { + if (output != NULL && + discovered < capacity) { + TrainlogMtpStorage *item = + &output[discovered]; + + (void)memset( + item, + 0, + sizeof(*item) + ); + + item->storage_id = + storage->id; + + item->storage_type = + storage->StorageType; + + item->filesystem_type = + storage->FilesystemType; + + item->access_capability = + storage->AccessCapability; + + item->max_capacity_bytes = + storage->MaxCapacity; + + item->free_space_bytes = + storage->FreeSpaceInBytes; + + item->free_space_objects = + storage->FreeSpaceInObjects; + + mtp_copy_text( + item->description, + sizeof(item->description), + storage->StorageDescription + ); + + mtp_copy_text( + item->volume_identifier, + sizeof(item->volume_identifier), + storage->VolumeIdentifier + ); + } + + ++discovered; + storage = storage->next; + } + + *output_count = + discovered; + + if (output != NULL && + discovered > capacity) { + status = + TRAINLOG_STATUS_INVALID_ARGUMENT; + } else { + status = + TRAINLOG_STATUS_OK; + } + + LIBMTP_Release_Device(device); + free(raw_devices); + + return status; +} + +static TrainlogStatus mtp_open_exact_device( + unsigned int bus_number, + unsigned int device_number, + LIBMTP_mtpdevice_t **output_device +) +{ + LIBMTP_raw_device_t *raw_devices = NULL; + LIBMTP_mtpdevice_t *device = NULL; + TrainlogStatus status; + int raw_count = 0; + int index; + + if (output_device == NULL || + device_number > UINT8_MAX) { + return TRAINLOG_STATUS_INVALID_ARGUMENT; + } + + *output_device = NULL; + + status = + mtp_detect_raw_devices( + &raw_devices, + &raw_count + ); + + if (status != TRAINLOG_STATUS_OK) { + free(raw_devices); + return status; + } + + for (index = 0; + index < raw_count; + ++index) { + if (mtp_raw_device_matches( + &raw_devices[index], + bus_number, + device_number + )) { + device = + LIBMTP_Open_Raw_Device( + &raw_devices[index] + ); + break; + } + } + + free(raw_devices); + + if (device == NULL) { + return TRAINLOG_STATUS_NOT_FOUND; + } + + *output_device = device; + + return TRAINLOG_STATUS_OK; +} + +static TrainlogStatus mtp_open_exact_device_uncached( + unsigned int bus_number, + unsigned int device_number, + LIBMTP_mtpdevice_t **output_device +) +{ + LIBMTP_raw_device_t *raw_devices = NULL; + LIBMTP_mtpdevice_t *device = NULL; + TrainlogStatus status; + int raw_count = 0; + int index; + + if (output_device == NULL || + device_number > UINT8_MAX) { + return TRAINLOG_STATUS_INVALID_ARGUMENT; + } + + *output_device = NULL; + + status = + mtp_detect_raw_devices( + &raw_devices, + &raw_count + ); + + if (status != TRAINLOG_STATUS_OK) { + free(raw_devices); + return status; + } + + for (index = 0; + index < raw_count; + ++index) { + if (mtp_raw_device_matches( + &raw_devices[index], + bus_number, + device_number + )) { + device = + LIBMTP_Open_Raw_Device_Uncached( + &raw_devices[index] + ); + break; + } + } + + free(raw_devices); + + if (device == NULL) { + return TRAINLOG_STATUS_NOT_FOUND; + } + + *output_device = device; + + return TRAINLOG_STATUS_OK; +} + +static LIBMTP_folder_t *mtp_find_root_folder( + LIBMTP_folder_t *folders, + uint32_t storage_id, + const char *folder_name +) +{ + LIBMTP_folder_t *folder; + + if (folder_name == NULL) { + return NULL; + } + + for (folder = folders; + folder != NULL; + folder = folder->sibling) { + if (folder->storage_id == storage_id && + folder->name != NULL && + strcmp( + folder->name, + folder_name + ) == 0) { + return folder; + } + } + + return NULL; +} + +static char *mtp_duplicate_text( + const char *text +) +{ + size_t length; + char *copy; + + if (text == NULL) { + return NULL; + } + + length = strlen(text); + + copy = + malloc( + length + 1U + ); + + if (copy == NULL) { + return NULL; + } + + (void)memcpy( + copy, + text, + length + 1U + ); + + return copy; +} + +TrainlogStatus trainlog_mtp_ensure_root_folder( + unsigned int bus_number, + unsigned int device_number, + uint32_t storage_id, + const char *folder_name, + uint32_t *output_folder_id, + bool *output_created +) +{ + LIBMTP_mtpdevice_t *device = NULL; + LIBMTP_folder_t *folders = NULL; + LIBMTP_folder_t *existing; + TrainlogStatus status; + uint32_t folder_id; + char mutable_name[256]; + + if (folder_name == NULL || + folder_name[0] == '\0' || + output_folder_id == NULL || + output_created == NULL || + strlen(folder_name) >= sizeof(mutable_name)) { + return TRAINLOG_STATUS_INVALID_ARGUMENT; + } + + *output_folder_id = 0U; + *output_created = false; + + status = + mtp_open_exact_device( + bus_number, + device_number, + &device + ); + + if (status != TRAINLOG_STATUS_OK) { + return status; + } + + folders = + LIBMTP_Get_Folder_List_For_Storage( + device, + storage_id + ); + + existing = + mtp_find_root_folder( + folders, + storage_id, + folder_name + ); + + if (existing != NULL) { + *output_folder_id = + existing->folder_id; + + if (folders != NULL) { + LIBMTP_destroy_folder_t(folders); + } + + LIBMTP_Release_Device(device); + + return TRAINLOG_STATUS_OK; + } + + (void)snprintf( + mutable_name, + sizeof(mutable_name), + "%s", + folder_name + ); + + folder_id = + LIBMTP_Create_Folder( + device, + mutable_name, + UINT32_MAX, + storage_id + ); + + if (folders != NULL) { + LIBMTP_destroy_folder_t(folders); + } + + if (folder_id == 0U) { + LIBMTP_Clear_Errorstack(device); + LIBMTP_Release_Device(device); + return TRAINLOG_STATUS_SYSTEM_ERROR; + } + + *output_folder_id = folder_id; + *output_created = true; + + LIBMTP_Release_Device(device); + + return TRAINLOG_STATUS_OK; +} + +TrainlogStatus trainlog_mtp_send_text_file( + unsigned int bus_number, + unsigned int device_number, + uint32_t storage_id, + uint32_t parent_folder_id, + const char *local_path, + const char *remote_filename, + uint32_t *output_item_id +) +{ + struct stat file_stat; + LIBMTP_mtpdevice_t *device = NULL; + LIBMTP_file_t *metadata = NULL; + TrainlogStatus status; + int rc; + + if (local_path == NULL || + local_path[0] == '\0' || + remote_filename == NULL || + remote_filename[0] == '\0' || + output_item_id == NULL) { + return TRAINLOG_STATUS_INVALID_ARGUMENT; + } + + *output_item_id = 0U; + + if (stat( + local_path, + &file_stat + ) != 0 || + file_stat.st_size < 0) { + return TRAINLOG_STATUS_SYSTEM_ERROR; + } + + status = + mtp_open_exact_device( + bus_number, + device_number, + &device + ); + + if (status != TRAINLOG_STATUS_OK) { + return status; + } + + metadata = + LIBMTP_new_file_t(); + + if (metadata == NULL) { + LIBMTP_Release_Device(device); + return TRAINLOG_STATUS_SYSTEM_ERROR; + } + + metadata->filename = + mtp_duplicate_text( + remote_filename + ); + + if (metadata->filename == NULL) { + LIBMTP_destroy_file_t(metadata); + LIBMTP_Release_Device(device); + return TRAINLOG_STATUS_SYSTEM_ERROR; + } + + metadata->parent_id = + parent_folder_id; + + metadata->storage_id = + storage_id; + + metadata->filesize = + (uint64_t)file_stat.st_size; + + metadata->filetype = + LIBMTP_FILETYPE_TEXT; + + rc = + LIBMTP_Send_File_From_File( + device, + local_path, + metadata, + NULL, + NULL + ); + + if (rc != 0) { + LIBMTP_Clear_Errorstack(device); + LIBMTP_destroy_file_t(metadata); + LIBMTP_Release_Device(device); + return TRAINLOG_STATUS_SYSTEM_ERROR; + } + + *output_item_id = + metadata->item_id; + + LIBMTP_destroy_file_t(metadata); + LIBMTP_Release_Device(device); + + return TRAINLOG_STATUS_OK; +} + +TrainlogStatus trainlog_mtp_list_folder( + unsigned int bus_number, + unsigned int device_number, + uint32_t storage_id, + uint32_t parent_folder_id, + TrainlogMtpEntry *output, + size_t capacity, + size_t *output_count +) +{ + LIBMTP_mtpdevice_t *device = NULL; + LIBMTP_file_t *items = NULL; + LIBMTP_file_t *item; + TrainlogStatus status; + size_t discovered = 0U; + + if (output_count == NULL || + (output == NULL && + capacity != 0U)) { + return + TRAINLOG_STATUS_INVALID_ARGUMENT; + } + + *output_count = 0U; + + status = + mtp_open_exact_device_uncached( + bus_number, + device_number, + &device + ); + + if (status != TRAINLOG_STATUS_OK) { + return status; + } + + items = + LIBMTP_Get_Files_And_Folders( + device, + storage_id, + parent_folder_id + ); + + for (item = items; + item != NULL; + item = item->next) { + if (output != NULL && + discovered < capacity) { + TrainlogMtpEntry *entry = + &output[discovered]; + + (void)memset( + entry, + 0, + sizeof(*entry) + ); + + entry->item_id = + item->item_id; + + entry->parent_id = + item->parent_id; + + entry->storage_id = + item->storage_id; + + entry->size_bytes = + item->filesize; + + entry->folder = + item->filetype == + LIBMTP_FILETYPE_FOLDER; + + mtp_copy_text( + entry->name, + sizeof(entry->name), + item->filename + ); + } + + ++discovered; + } + + if (items != NULL) { + LIBMTP_destroy_file_t( + items + ); + } + + LIBMTP_Release_Device( + device + ); + + *output_count = + discovered; + + if (output != NULL && + discovered > capacity) { + return + TRAINLOG_STATUS_INVALID_ARGUMENT; + } + + return + TRAINLOG_STATUS_OK; +} + +TrainlogStatus trainlog_mtp_receive_file( + unsigned int bus_number, + unsigned int device_number, + uint32_t item_id, + const char *local_path +) +{ + LIBMTP_mtpdevice_t *device = NULL; + TrainlogStatus status; + int rc; + + if (item_id == 0U || + local_path == NULL || + local_path[0] == '\0') { + return + TRAINLOG_STATUS_INVALID_ARGUMENT; + } + + status = + mtp_open_exact_device_uncached( + bus_number, + device_number, + &device + ); + + if (status != TRAINLOG_STATUS_OK) { + return status; + } + + rc = + LIBMTP_Get_File_To_File( + device, + item_id, + local_path, + NULL, + NULL + ); + + if (rc != 0) { + LIBMTP_Clear_Errorstack( + device + ); + + LIBMTP_Release_Device( + device + ); + + return + TRAINLOG_STATUS_SYSTEM_ERROR; + } + + LIBMTP_Release_Device( + device + ); + + return + TRAINLOG_STATUS_OK; +} diff --git a/tui/src/usb.c b/tui/src/usb.c new file mode 100644 index 0000000..38d13e4 --- /dev/null +++ b/tui/src/usb.c @@ -0,0 +1,386 @@ +#include "trainlog/usb.h" + +#include +#include +#include +#include +#include + +#include + +static const char *usb_property_prefer( + struct udev_device *device, + const char *preferred, + const char *fallback +) +{ + const char *value = + udev_device_get_property_value( + device, + preferred + ); + + if (value != NULL && + value[0] != '\0') { + return value; + } + + return + udev_device_get_property_value( + device, + fallback + ); +} + +static void usb_copy_text( + char *output, + size_t capacity, + const char *value +) +{ + if (output == NULL || + capacity == 0U) { + return; + } + + if (value == NULL) { + output[0] = '\0'; + return; + } + + (void)snprintf( + output, + capacity, + "%s", + value + ); +} + +static bool usb_parse_unsigned( + const char *text, + int base, + unsigned int *output +) +{ + char *end = NULL; + unsigned long value; + + if (text == NULL || + output == NULL || + text[0] == '\0') { + return false; + } + + errno = 0; + + value = + strtoul( + text, + &end, + base + ); + + if (errno != 0 || + end == text || + *end != '\0' || + value > (unsigned long)UINT_MAX) { + return false; + } + + *output = + (unsigned int)value; + + return true; +} + +static bool usb_device_is_physical_mtp( + struct udev_device *device +) +{ + const char *devtype; + const char *mtp; + + if (device == NULL) { + return false; + } + + devtype = + udev_device_get_devtype( + device + ); + + if (devtype == NULL || + strcmp( + devtype, + "usb_device" + ) != 0) { + return false; + } + + mtp = + udev_device_get_property_value( + device, + "ID_MTP_DEVICE" + ); + + return + mtp != NULL && + strcmp(mtp, "1") == 0; +} + +static void usb_fill_device( + struct udev_device *device, + TrainlogUsbDevice *output +) +{ + const char *vendor; + const char *model; + const char *serial; + const char *vendor_id; + const char *product_id; + const char *bus; + const char *devnum; + const char *syspath; + + (void)memset( + output, + 0, + sizeof(*output) + ); + + output->mtp = true; + + vendor = + usb_property_prefer( + device, + "ID_VENDOR_FROM_DATABASE", + "ID_VENDOR" + ); + + model = + usb_property_prefer( + device, + "ID_MODEL_FROM_DATABASE", + "ID_MODEL" + ); + + serial = + udev_device_get_property_value( + device, + "ID_SERIAL_SHORT" + ); + + vendor_id = + udev_device_get_property_value( + device, + "ID_VENDOR_ID" + ); + + product_id = + udev_device_get_property_value( + device, + "ID_MODEL_ID" + ); + + bus = + udev_device_get_property_value( + device, + "BUSNUM" + ); + + devnum = + udev_device_get_property_value( + device, + "DEVNUM" + ); + + if (bus == NULL) { + bus = + udev_device_get_sysattr_value( + device, + "busnum" + ); + } + + if (devnum == NULL) { + devnum = + udev_device_get_sysattr_value( + device, + "devnum" + ); + } + + syspath = + udev_device_get_syspath( + device + ); + + usb_copy_text( + output->vendor, + sizeof(output->vendor), + vendor + ); + + usb_copy_text( + output->model, + sizeof(output->model), + model + ); + + usb_copy_text( + output->serial, + sizeof(output->serial), + serial + ); + + usb_copy_text( + output->syspath, + sizeof(output->syspath), + syspath + ); + + (void)usb_parse_unsigned( + vendor_id, + 16, + &output->vendor_id + ); + + (void)usb_parse_unsigned( + product_id, + 16, + &output->product_id + ); + + (void)usb_parse_unsigned( + bus, + 10, + &output->bus_number + ); + + (void)usb_parse_unsigned( + devnum, + 10, + &output->device_number + ); +} + +TrainlogStatus trainlog_usb_list_mtp_devices( + TrainlogUsbDevice *output, + size_t capacity, + size_t *output_count +) +{ + struct udev *udev = NULL; + struct udev_enumerate *enumerate = NULL; + struct udev_list_entry *devices; + struct udev_list_entry *entry; + size_t discovered = 0U; + TrainlogStatus status = + TRAINLOG_STATUS_OK; + + if (output_count == NULL || + (output == NULL && + capacity != 0U)) { + return + TRAINLOG_STATUS_INVALID_ARGUMENT; + } + + *output_count = 0U; + + udev = udev_new(); + + if (udev == NULL) { + return + TRAINLOG_STATUS_SYSTEM_ERROR; + } + + enumerate = + udev_enumerate_new(udev); + + if (enumerate == NULL) { + udev_unref(udev); + return + TRAINLOG_STATUS_SYSTEM_ERROR; + } + + if (udev_enumerate_add_match_subsystem( + enumerate, + "usb" + ) < 0 || + udev_enumerate_scan_devices( + enumerate + ) < 0) { + status = + TRAINLOG_STATUS_SYSTEM_ERROR; + goto cleanup; + } + + devices = + udev_enumerate_get_list_entry( + enumerate + ); + + udev_list_entry_foreach( + entry, + devices + ) { + const char *syspath = + udev_list_entry_get_name( + entry + ); + + struct udev_device *device; + + if (syspath == NULL) { + continue; + } + + device = + udev_device_new_from_syspath( + udev, + syspath + ); + + if (device == NULL) { + continue; + } + + if (usb_device_is_physical_mtp( + device + )) { + if (output != NULL && + discovered < capacity) { + usb_fill_device( + device, + &output[discovered] + ); + } + + ++discovered; + } + + udev_device_unref( + device + ); + } + + *output_count = + discovered; + + if (output != NULL && + discovered > capacity) { + status = + TRAINLOG_STATUS_INVALID_ARGUMENT; + } + +cleanup: + udev_enumerate_unref( + enumerate + ); + + udev_unref( + udev + ); + + return status; +} diff --git a/tui/tests/test_mtp.c b/tui/tests/test_mtp.c new file mode 100644 index 0000000..9cf1611 --- /dev/null +++ b/tui/tests/test_mtp.c @@ -0,0 +1,190 @@ +#include +#include + +#include "trainlog/mtp.h" +#include "trainlog/status.h" + +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + (void)fprintf( \ + stderr, \ + "CHECK failed: %s:%d: %s\n", \ + __FILE__, \ + __LINE__, \ + #condition \ + ); \ + return 1; \ + } \ + } while (0) + +static int test_argument_validation(void) +{ + size_t count = 0U; + TrainlogMtpStorage storage; + + CHECK( + trainlog_mtp_list_storages( + 1U, + 1U, + NULL, + 1U, + &count + ) == + TRAINLOG_STATUS_INVALID_ARGUMENT + ); + + CHECK( + trainlog_mtp_list_storages( + 1U, + 1U, + &storage, + 1U, + NULL + ) == + TRAINLOG_STATUS_INVALID_ARGUMENT + ); + + CHECK( + trainlog_mtp_list_storages( + 1U, + UINT_MAX, + &storage, + 1U, + &count + ) == + TRAINLOG_STATUS_INVALID_ARGUMENT + ); + + return 0; +} + +static int test_exchange_argument_validation(void) +{ + uint32_t folder_id = 0U; + uint32_t item_id = 0U; + bool created = false; + + CHECK( + trainlog_mtp_ensure_root_folder( + 1U, + 1U, + 1U, + NULL, + &folder_id, + &created + ) == + TRAINLOG_STATUS_INVALID_ARGUMENT + ); + + CHECK( + trainlog_mtp_ensure_root_folder( + 1U, + 1U, + 1U, + "", + &folder_id, + &created + ) == + TRAINLOG_STATUS_INVALID_ARGUMENT + ); + + CHECK( + trainlog_mtp_send_text_file( + 1U, + 1U, + 1U, + 1U, + NULL, + "probe.txt", + &item_id + ) == + TRAINLOG_STATUS_INVALID_ARGUMENT + ); + + CHECK( + trainlog_mtp_send_text_file( + 1U, + 1U, + 1U, + 1U, + "/definitely/not/present/trainlog-probe", + "probe.txt", + &item_id + ) == + TRAINLOG_STATUS_SYSTEM_ERROR + ); + + return 0; +} + +static int test_roundtrip_argument_validation(void) +{ + size_t count = 0U; + TrainlogMtpEntry entry; + + CHECK( + trainlog_mtp_list_folder( + 1U, + 1U, + 1U, + 1U, + NULL, + 1U, + &count + ) == + TRAINLOG_STATUS_INVALID_ARGUMENT + ); + + CHECK( + trainlog_mtp_list_folder( + 1U, + 1U, + 1U, + 1U, + &entry, + 1U, + NULL + ) == + TRAINLOG_STATUS_INVALID_ARGUMENT + ); + + CHECK( + trainlog_mtp_receive_file( + 1U, + 1U, + 0U, + "/tmp/trainlog-invalid" + ) == + TRAINLOG_STATUS_INVALID_ARGUMENT + ); + + CHECK( + trainlog_mtp_receive_file( + 1U, + 1U, + 1U, + NULL + ) == + TRAINLOG_STATUS_INVALID_ARGUMENT + ); + + return 0; +} + +int main(void) +{ + CHECK( + test_argument_validation() == 0 + ); + + CHECK( + test_exchange_argument_validation() == 0 + ); + + CHECK( + test_roundtrip_argument_validation() == 0 + ); + + return 0; +} diff --git a/tui/tests/test_usb.c b/tui/tests/test_usb.c new file mode 100644 index 0000000..be8dbac --- /dev/null +++ b/tui/tests/test_usb.c @@ -0,0 +1,129 @@ +#include +#include + +#include "trainlog/status.h" +#include "trainlog/usb.h" + +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + (void)fprintf( \ + stderr, \ + "CHECK failed: %s:%d: %s\n", \ + __FILE__, \ + __LINE__, \ + #condition \ + ); \ + return 1; \ + } \ + } while (0) + +#define TEST_CAPACITY 32U + +static int test_argument_validation(void) +{ + size_t count = 0U; + TrainlogUsbDevice device; + + CHECK( + trainlog_usb_list_mtp_devices( + NULL, + 1U, + &count + ) == + TRAINLOG_STATUS_INVALID_ARGUMENT + ); + + CHECK( + trainlog_usb_list_mtp_devices( + &device, + 1U, + NULL + ) == + TRAINLOG_STATUS_INVALID_ARGUMENT + ); + + return 0; +} + +static int test_live_enumeration(void) +{ + TrainlogUsbDevice + devices[TEST_CAPACITY]; + + size_t count_only = 0U; + size_t count = 0U; + size_t index; + size_t other; + + CHECK( + trainlog_usb_list_mtp_devices( + NULL, + 0U, + &count_only + ) == + TRAINLOG_STATUS_OK + ); + + CHECK( + count_only <= + TEST_CAPACITY + ); + + CHECK( + trainlog_usb_list_mtp_devices( + devices, + TEST_CAPACITY, + &count + ) == + TRAINLOG_STATUS_OK + ); + + CHECK(count == count_only); + + for (index = 0U; + index < count; + ++index) { + CHECK( + devices[index].mtp + ); + + CHECK( + devices[index].syspath[0] != + '\0' + ); + + CHECK( + strstr( + devices[index].syspath, + ":1." + ) == NULL + ); + + for (other = index + 1U; + other < count; + ++other) { + CHECK( + devices[index].bus_number != + devices[other].bus_number || + devices[index].device_number != + devices[other].device_number + ); + } + } + + return 0; +} + +int main(void) +{ + CHECK( + test_argument_validation() == 0 + ); + + CHECK( + test_live_enumeration() == 0 + ); + + return 0; +} diff --git a/tui/tools/mtp_exchange_probe.c b/tui/tools/mtp_exchange_probe.c new file mode 100644 index 0000000..20726ff --- /dev/null +++ b/tui/tools/mtp_exchange_probe.c @@ -0,0 +1,186 @@ +#include +#include +#include +#include +#include + +#include "trainlog/mtp.h" +#include "trainlog/status.h" +#include "trainlog/usb.h" + +#define DEVICE_CAPACITY 16U +#define STORAGE_CAPACITY 16U + +static int create_probe_file( + char path[64] +) +{ + static const char content[] = + "Trainlog MTP write probe\n" + "If you can read this file on Android, USB/MTP write works.\n"; + + int fd; + ssize_t written; + + (void)snprintf( + path, + 64U, + "%s", + "/tmp/trainlog-mtp-probe-XXXXXX" + ); + + fd = mkstemp(path); + + if (fd < 0) { + return -1; + } + + written = + write( + fd, + content, + sizeof(content) - 1U + ); + + if (written != + (ssize_t)(sizeof(content) - 1U)) { + (void)close(fd); + (void)unlink(path); + return -1; + } + + if (close(fd) != 0) { + (void)unlink(path); + return -1; + } + + return 0; +} + +int main(void) +{ + TrainlogUsbDevice + devices[DEVICE_CAPACITY]; + + TrainlogMtpStorage + storages[STORAGE_CAPACITY]; + + size_t device_count = 0U; + size_t storage_count = 0U; + uint32_t folder_id = 0U; + uint32_t item_id = 0U; + bool created = false; + char local_path[64]; + TrainlogStatus status; + + status = + trainlog_usb_list_mtp_devices( + devices, + DEVICE_CAPACITY, + &device_count + ); + + if (status != TRAINLOG_STATUS_OK || + device_count == 0U) { + (void)fprintf( + stderr, + "No physical MTP device available.\n" + ); + return 1; + } + + status = + trainlog_mtp_list_storages( + devices[0].bus_number, + devices[0].device_number, + storages, + STORAGE_CAPACITY, + &storage_count + ); + + if (status != TRAINLOG_STATUS_OK || + storage_count == 0U) { + (void)fprintf( + stderr, + "No MTP storage available.\n" + ); + return 1; + } + + (void)printf( + "Using %s / %s\n", + devices[0].vendor, + devices[0].model + ); + + (void)printf( + "Storage: %s (0x%08x)\n", + storages[0].description, + storages[0].storage_id + ); + + status = + trainlog_mtp_ensure_root_folder( + devices[0].bus_number, + devices[0].device_number, + storages[0].storage_id, + "Trainlog", + &folder_id, + &created + ); + + if (status != TRAINLOG_STATUS_OK) { + (void)fprintf( + stderr, + "Unable to ensure Trainlog folder: %d\n", + (int)status + ); + return 1; + } + + (void)printf( + "Trainlog folder id=0x%08x (%s)\n", + folder_id, + created + ? "created" + : "already present" + ); + + if (create_probe_file(local_path) != 0) { + (void)fprintf( + stderr, + "Unable to create local probe file.\n" + ); + return 1; + } + + status = + trainlog_mtp_send_text_file( + devices[0].bus_number, + devices[0].device_number, + storages[0].storage_id, + folder_id, + local_path, + "trainlog-probe.txt", + &item_id + ); + + (void)unlink(local_path); + + if (status != TRAINLOG_STATUS_OK) { + (void)fprintf( + stderr, + "Unable to send probe file: %d\n", + (int)status + ); + return 1; + } + + (void)printf( + "Uploaded Trainlog/trainlog-probe.txt " + "item_id=0x%08x\n", + item_id + ); + + return 0; +} diff --git a/tui/tools/mtp_probe.c b/tui/tools/mtp_probe.c new file mode 100644 index 0000000..1c70b55 --- /dev/null +++ b/tui/tools/mtp_probe.c @@ -0,0 +1,161 @@ +/** + * @file mtp_probe.c + * @brief Manual end-to-end USB -> MTP storage probe. + */ + +#include +#include + +#include "trainlog/mtp.h" +#include "trainlog/status.h" +#include "trainlog/usb.h" + +#define DEVICE_CAPACITY 16U +#define STORAGE_CAPACITY 16U + +static double bytes_to_gib( + uint64_t bytes +) +{ + return + (double)bytes / + (1024.0 * 1024.0 * 1024.0); +} + +int main(void) +{ + TrainlogUsbDevice + devices[DEVICE_CAPACITY]; + + size_t device_count = 0U; + size_t device_index; + TrainlogStatus status; + + status = + trainlog_usb_list_mtp_devices( + devices, + DEVICE_CAPACITY, + &device_count + ); + + if (status != TRAINLOG_STATUS_OK) { + (void)fprintf( + stderr, + "USB MTP discovery failed: %d\n", + (int)status + ); + + return 1; + } + + (void)printf( + "Physical MTP devices: %zu\n", + device_count + ); + + for (device_index = 0U; + device_index < device_count; + ++device_index) { + TrainlogMtpStorage + storages[STORAGE_CAPACITY]; + + const TrainlogUsbDevice *usb = + &devices[device_index]; + + size_t storage_count = 0U; + size_t storage_index; + + (void)printf( + "\n[%zu] %s / %s\n", + device_index, + usb->vendor[0] != '\0' + ? usb->vendor + : "(unknown vendor)", + usb->model[0] != '\0' + ? usb->model + : "(unknown model)" + ); + + (void)printf( + " usb=%03u:%03u vid:pid=%04x:%04x\n", + usb->bus_number, + usb->device_number, + usb->vendor_id, + usb->product_id + ); + + status = + trainlog_mtp_list_storages( + usb->bus_number, + usb->device_number, + storages, + STORAGE_CAPACITY, + &storage_count + ); + + if (status != TRAINLOG_STATUS_OK) { + (void)printf( + " libmtp open/storage failed: %d\n", + (int)status + ); + + continue; + } + + (void)printf( + " storages=%zu\n", + storage_count + ); + + for (storage_index = 0U; + storage_index < storage_count; + ++storage_index) { + const TrainlogMtpStorage *storage = + &storages[storage_index]; + + (void)printf( + " storage[%zu] id=0x%08" PRIx32 "\n", + storage_index, + storage->storage_id + ); + + (void)printf( + " description=%s\n", + storage->description[0] != '\0' + ? storage->description + : "(none)" + ); + + (void)printf( + " volume=%s\n", + storage->volume_identifier[0] != '\0' + ? storage->volume_identifier + : "(none)" + ); + + (void)printf( + " capacity=%.2f GiB free=%.2f GiB\n", + bytes_to_gib( + storage->max_capacity_bytes + ), + bytes_to_gib( + storage->free_space_bytes + ) + ); + + (void)printf( + " access=0x%04x " + "storage_type=0x%04x " + "fs=0x%04x\n", + (unsigned int) + storage->access_capability, + (unsigned int) + storage->storage_type, + (unsigned int) + storage->filesystem_type + ); + } + } + + return 0; +} diff --git a/tui/tools/mtp_roundtrip_probe.c b/tui/tools/mtp_roundtrip_probe.c new file mode 100644 index 0000000..adef925 --- /dev/null +++ b/tui/tools/mtp_roundtrip_probe.c @@ -0,0 +1,273 @@ +#include +#include +#include +#include +#include +#include + +#include "trainlog/mtp.h" +#include "trainlog/status.h" +#include "trainlog/usb.h" + +#define DEVICE_CAPACITY 16U +#define STORAGE_CAPACITY 16U +#define ENTRY_CAPACITY 128U + +static const char EXPECTED_CONTENT[] = + "Trainlog MTP write probe\n" + "If you can read this file on Android, USB/MTP write works.\n"; + +static int verify_file( + const char *path +) +{ + FILE *stream; + char buffer[256]; + size_t read_size; + + stream = + fopen( + path, + "rb" + ); + + if (stream == NULL) { + return -1; + } + + read_size = + fread( + buffer, + 1U, + sizeof(buffer), + stream + ); + + if (ferror(stream) != 0) { + (void)fclose(stream); + return -1; + } + + if (fclose(stream) != 0) { + return -1; + } + + if (read_size != + sizeof(EXPECTED_CONTENT) - 1U) { + return -1; + } + + if (memcmp( + buffer, + EXPECTED_CONTENT, + read_size + ) != 0) { + return -1; + } + + return 0; +} + +int main(void) +{ + TrainlogUsbDevice + devices[DEVICE_CAPACITY]; + + TrainlogMtpStorage + storages[STORAGE_CAPACITY]; + + TrainlogMtpEntry + entries[ENTRY_CAPACITY]; + + size_t device_count = 0U; + size_t storage_count = 0U; + size_t entry_count = 0U; + size_t index; + uint32_t folder_id = 0U; + uint32_t probe_item_id = 0U; + bool created = false; + char local_path[] = + "/tmp/trainlog-mtp-readback-XXXXXX"; + int fd; + TrainlogStatus status; + + status = + trainlog_usb_list_mtp_devices( + devices, + DEVICE_CAPACITY, + &device_count + ); + + if (status != TRAINLOG_STATUS_OK || + device_count == 0U) { + (void)fprintf( + stderr, + "No physical MTP device available.\n" + ); + return 1; + } + + status = + trainlog_mtp_list_storages( + devices[0].bus_number, + devices[0].device_number, + storages, + STORAGE_CAPACITY, + &storage_count + ); + + if (status != TRAINLOG_STATUS_OK || + storage_count == 0U) { + (void)fprintf( + stderr, + "No MTP storage available.\n" + ); + return 1; + } + + status = + trainlog_mtp_ensure_root_folder( + devices[0].bus_number, + devices[0].device_number, + storages[0].storage_id, + "Trainlog", + &folder_id, + &created + ); + + if (status != TRAINLOG_STATUS_OK) { + (void)fprintf( + stderr, + "Unable to locate Trainlog folder: %d\n", + (int)status + ); + return 1; + } + + (void)printf( + "Trainlog folder id=0x%08x (%s)\n", + folder_id, + created + ? "created" + : "already present" + ); + + status = + trainlog_mtp_list_folder( + devices[0].bus_number, + devices[0].device_number, + storages[0].storage_id, + folder_id, + entries, + ENTRY_CAPACITY, + &entry_count + ); + + if (status != TRAINLOG_STATUS_OK) { + (void)fprintf( + stderr, + "Unable to list Trainlog folder: %d\n", + (int)status + ); + return 1; + } + + (void)printf( + "Trainlog entries: %zu\n", + entry_count + ); + + for (index = 0U; + index < entry_count; + ++index) { + (void)printf( + " %s id=0x%08x %s size=%llu\n", + entries[index].folder + ? "[DIR] " + : "[FILE]", + entries[index].item_id, + entries[index].name, + (unsigned long long) + entries[index].size_bytes + ); + + if (!entries[index].folder && + strcmp( + entries[index].name, + "trainlog-probe.txt" + ) == 0) { + probe_item_id = + entries[index].item_id; + } + } + + if (probe_item_id == 0U) { + (void)fprintf( + stderr, + "trainlog-probe.txt not found.\n" + ); + return 1; + } + + fd = + mkstemp( + local_path + ); + + if (fd < 0) { + (void)fprintf( + stderr, + "Unable to create local readback file.\n" + ); + return 1; + } + + if (close(fd) != 0) { + (void)unlink(local_path); + return 1; + } + + status = + trainlog_mtp_receive_file( + devices[0].bus_number, + devices[0].device_number, + probe_item_id, + local_path + ); + + if (status != TRAINLOG_STATUS_OK) { + (void)unlink(local_path); + + (void)fprintf( + stderr, + "Unable to download probe file: %d\n", + (int)status + ); + + return 1; + } + + if (verify_file( + local_path + ) != 0) { + (void)unlink(local_path); + + (void)fprintf( + stderr, + "Readback content mismatch.\n" + ); + + return 1; + } + + (void)unlink( + local_path + ); + + (void)printf( + "ROUNDTRIP=PASS " + "Trainlog/trainlog-probe.txt\n" + ); + + return 0; +} diff --git a/tui/tools/usb_probe.c b/tui/tools/usb_probe.c new file mode 100644 index 0000000..e5f022b --- /dev/null +++ b/tui/tools/usb_probe.c @@ -0,0 +1,80 @@ +#include + +#include "trainlog/status.h" +#include "trainlog/usb.h" + +#define PROBE_CAPACITY 16U + +int main(void) +{ + TrainlogUsbDevice + devices[PROBE_CAPACITY]; + + size_t count = 0U; + size_t index; + TrainlogStatus status; + + status = + trainlog_usb_list_mtp_devices( + devices, + PROBE_CAPACITY, + &count + ); + + if (status != TRAINLOG_STATUS_OK) { + (void)fprintf( + stderr, + "MTP discovery failed: %d\n", + (int)status + ); + + return 1; + } + + (void)printf( + "MTP devices: %zu\n", + count + ); + + for (index = 0U; + index < count; + ++index) { + const TrainlogUsbDevice *device = + &devices[index]; + + (void)printf( + "[%zu] %s / %s\n", + index, + device->vendor[0] != '\0' + ? device->vendor + : "(unknown vendor)", + device->model[0] != '\0' + ? device->model + : "(unknown model)" + ); + + (void)printf( + " usb=%03u:%03u vid:pid=%04x:%04x\n", + device->bus_number, + device->device_number, + device->vendor_id, + device->product_id + ); + + (void)printf( + " serial=%s\n", + device->serial[0] != '\0' + ? device->serial + : "(none)" + ); + + (void)printf( + " syspath=%s\n", + device->syspath[0] != '\0' + ? device->syspath + : "(none)" + ); + } + + return 0; +}