Add direct Android MTP transport foundation
This commit is contained in:
parent
2d47805431
commit
c76d4c2106
17 changed files with 2641 additions and 0 deletions
16
CHANGELOG.md
16
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.
|
||||
<!-- 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 -->
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
112
docs/reviews/mtp_transport.md
Normal file
112
docs/reviews/mtp_transport.md
Normal 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
|
||||
```
|
||||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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
107
tui/include/trainlog/mtp.h
Normal 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
|
||||
32
tui/include/trainlog/usb.h
Normal file
32
tui/include/trainlog/usb.h
Normal 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
|
||||
|
|
@ -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
779
tui/src/mtp.c
Normal 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
386
tui/src/usb.c
Normal 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
190
tui/tests/test_mtp.c
Normal 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
129
tui/tests/test_usb.c
Normal 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;
|
||||
}
|
||||
186
tui/tools/mtp_exchange_probe.c
Normal file
186
tui/tools/mtp_exchange_probe.c
Normal 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
161
tui/tools/mtp_probe.c
Normal 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;
|
||||
}
|
||||
273
tui/tools/mtp_roundtrip_probe.c
Normal file
273
tui/tools/mtp_roundtrip_probe.c
Normal 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
80
tui/tools/usb_probe.c
Normal 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;
|
||||
}
|
||||
Loading…
Reference in a new issue