Add direct Android MTP transport foundation

This commit is contained in:
fy59 2026-09-06 10:33:28 +02:00
parent 2d47805431
commit c76d4c2106
17 changed files with 2641 additions and 0 deletions

View file

@ -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.
<!-- TRAINLOG_EDITABILITY_NAV_CHANGELOG _END -->
<!-- TRAINLOG_MTP_TRANSPORT_CHANGELOG -->
### 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.
<!-- TRAINLOG_MTP_TRANSPORT_CHANGELOG _END -->

View file

@ -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.
<!-- TRAINLOG_ANDROID_MTP_TRANSPORT -->
## 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.
<!-- TRAINLOG_ANDROID_MTP_TRANSPORT _END -->

View file

@ -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.
<!-- TRAINLOG_DIRECT_MTP_ARCHITECTURE -->
## 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.
<!-- TRAINLOG_DIRECT_MTP_ARCHITECTURE _END -->

View file

@ -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
```

View file

@ -198,3 +198,35 @@ ANDROID_MINIMAL_RECORDER=AFTER
JSON_V1_USB_IMPORT_EXPORT=AFTER
```
<!-- TRAINLOG_EDITABILITY_ROADMAP _END -->
<!-- TRAINLOG_MTP_TRANSPORT_ROADMAP -->
## 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
```
<!-- TRAINLOG_MTP_TRANSPORT_ROADMAP _END -->

View file

@ -153,3 +153,40 @@ python tools/validate_json.py
python tools/validate_import_contract.py
git diff --check
```
<!-- TRAINLOG_MTP_VALIDATION -->
## 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.
<!-- TRAINLOG_MTP_VALIDATION _END -->

107
tui/include/trainlog/mtp.h Normal file
View file

@ -0,0 +1,107 @@
#ifndef TRAINLOG_MTP_H
#define TRAINLOG_MTP_H
/**
* @file mtp.h
* @brief MTP device access and storage enumeration.
*/
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#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

View file

@ -0,0 +1,32 @@
#ifndef TRAINLOG_USB_H
#define TRAINLOG_USB_H
#include <stdbool.h>
#include <stddef.h>
#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

View file

@ -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,
)

779
tui/src/mtp.c Normal file
View file

@ -0,0 +1,779 @@
/**
* @file mtp.c
* @brief libmtp-backed storage access.
*/
#include "trainlog/mtp.h"
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <libmtp.h>
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;
}

386
tui/src/usb.c Normal file
View file

@ -0,0 +1,386 @@
#include "trainlog/usb.h"
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libudev.h>
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;
}

190
tui/tests/test_mtp.c Normal file
View file

@ -0,0 +1,190 @@
#include <limits.h>
#include <stdio.h>
#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;
}

129
tui/tests/test_usb.c Normal file
View file

@ -0,0 +1,129 @@
#include <stdio.h>
#include <string.h>
#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;
}

View file

@ -0,0 +1,186 @@
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#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;
}

161
tui/tools/mtp_probe.c Normal file
View file

@ -0,0 +1,161 @@
/**
* @file mtp_probe.c
* @brief Manual end-to-end USB -> MTP storage probe.
*/
#include <inttypes.h>
#include <stdio.h>
#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;
}

View file

@ -0,0 +1,273 @@
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#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;
}

80
tui/tools/usb_probe.c Normal file
View file

@ -0,0 +1,80 @@
#include <stdio.h>
#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;
}