cadmus_core/device/usb/kobo/
mtk.rs

1//! MTK (MediaTek) USB gadget implementation using ConfigFS.
2//!
3//! This module provides USB mass storage support for MTK-based Kobo devices
4//! (platform `mt8113t-ntx`). It uses ConfigFS to configure the USB gadget
5//! instead of shell commands.
6//!
7//! # ConfigFS write convention
8//!
9//! All values are written with a trailing `\n` to match the behaviour of the
10//! `echo` shell command used in the [Linux kernel ConfigFS gadget documentation](https://www.kernel.org/doc/html/latest/usb/gadget_configfs.html)
11//! examples. The kernel docs do not explicitly require a newline, but omitting
12//! it caused the UDC bind to silently fail on the Libra Colour (MTK platform)
13//! in practice.
14
15use crate::device::metadata::DeviceMetadata;
16use crate::device::usb::error::UsbError;
17use crate::device::usb::kobo::operations::KoboUsbOperations;
18use crate::device::usb::manager::UsbManager;
19use std::fs;
20use std::path::Path;
21use tracing::{debug, error, info, warn};
22
23/// Base path for ConfigFS USB gadget configuration.
24const CONFIGFS_GADGET_DIR: &str = "/sys/kernel/config/usb_gadget/g1";
25
26/// Path to the UDC directory for auto-discovery.
27const UDC_DIR: &str = "/sys/class/udc";
28
29/// Discovers the USB Device Controller (UDC) name.
30///
31/// Reads the `/sys/class/udc/` directory to find available UDCs. For MTK
32/// platforms, this is typically `"11211000.usb"`.
33///
34/// # Errors
35///
36/// Returns [`UsbError::Udc`] if no UDC is available or the directory cannot
37/// be read.
38fn discover_udc() -> Result<String, UsbError> {
39    let udc_path = Path::new(UDC_DIR);
40
41    if !udc_path.exists() {
42        warn!(path = UDC_DIR, "UDC directory does not exist");
43        return Err(UsbError::Udc("UDC directory not found".to_string()));
44    }
45
46    let mut entries = fs::read_dir(udc_path).map_err(|e| {
47        error!(path = UDC_DIR, error = %e, "Failed to read UDC directory");
48        UsbError::Udc(format!("Cannot read UDC directory: {}", e))
49    })?;
50
51    let entry = entries
52        .next()
53        .ok_or_else(|| UsbError::Udc("No UDC available".to_string()))?;
54    let entry = entry.map_err(|e| UsbError::Udc(format!("Cannot read UDC entry: {}", e)))?;
55    let name = entry
56        .file_name()
57        .into_string()
58        .map_err(|_| UsbError::Udc("UDC name contains invalid UTF-8".to_string()))?;
59
60    debug!(udc_name = %name, "Found UDC");
61    Ok(name)
62}
63
64/// USB mass storage manager for MTK platforms.
65///
66/// This implementation configures the USB gadget via ConfigFS, which is the
67/// modern approach for MTK-based Kobo devices. It creates the gadget
68/// configuration, sets up the mass storage function, and binds to the UDC.
69pub struct MtkUsbManager {
70    metadata: DeviceMetadata,
71    udc: String,
72}
73
74impl MtkUsbManager {
75    /// Creates a new MTK USB manager.
76    ///
77    /// Discovers the UDC and prepares for gadget setup. No USB operations
78    /// are performed until [`enable`](UsbManager::enable) is called.
79    ///
80    /// # Errors
81    ///
82    /// Returns [`UsbError::Udc`] if no UDC is available or the UDC
83    /// directory cannot be read.
84    #[cfg_attr(feature = "otel", tracing::instrument(skip(metadata)))]
85    pub fn new(metadata: DeviceMetadata) -> Result<Self, UsbError> {
86        let udc = discover_udc()?;
87        info!(
88            vendor_id = metadata.vendor_id,
89            product_id = metadata.product_id,
90            serial_number = %metadata.serial_number,
91            partition = %metadata.partition,
92            udc = %udc,
93            "MtkUsbManager constructed"
94        );
95        Ok(Self { metadata, udc })
96    }
97
98    /// Creates the ConfigFS gadget directory structure.
99    fn create_gadget_dirs(&self) -> Result<(), UsbError> {
100        debug!("Creating ConfigFS gadget directories");
101
102        let dirs = [
103            format!("{}/strings/0x409", CONFIGFS_GADGET_DIR),
104            format!("{}/configs/c.1/strings/0x409", CONFIGFS_GADGET_DIR),
105            format!("{}/functions/mass_storage.0/lun.0", CONFIGFS_GADGET_DIR),
106        ];
107
108        for dir in &dirs {
109            fs::create_dir_all(dir).map_err(|e| {
110                error!(directory = %dir, error = %e, "Failed to create ConfigFS directory");
111                UsbError::GadgetConfig(format!("Cannot create directory {}: {}", dir, e))
112            })?;
113        }
114
115        Ok(())
116    }
117
118    /// Writes gadget configuration to ConfigFS.
119    ///
120    /// All values are written with a trailing `\n` to match `echo` behaviour from
121    /// the kernel documentation examples.
122    fn write_gadget_config(&self) -> Result<(), UsbError> {
123        debug!("Writing gadget configuration to ConfigFS");
124
125        let base = Path::new(CONFIGFS_GADGET_DIR);
126
127        fs::write(
128            base.join("idVendor"),
129            format!("0x{:04X}\n", self.metadata.vendor_id),
130        )
131        .map_err(|e| UsbError::GadgetConfig(format!("Cannot write idVendor: {}", e)))?;
132
133        fs::write(
134            base.join("idProduct"),
135            format!("0x{:04X}\n", self.metadata.product_id),
136        )
137        .map_err(|e| UsbError::GadgetConfig(format!("Cannot write idProduct: {}", e)))?;
138
139        let strings = base.join("strings/0x409");
140        fs::write(
141            strings.join("serialnumber"),
142            format!("{}\n", self.metadata.serial_number),
143        )
144        .map_err(|e| UsbError::GadgetConfig(format!("Cannot write serialnumber: {}", e)))?;
145
146        fs::write(
147            strings.join("manufacturer"),
148            format!("{}\n", self.metadata.manufacturer),
149        )
150        .map_err(|e| UsbError::GadgetConfig(format!("Cannot write manufacturer: {}", e)))?;
151
152        fs::write(
153            strings.join("product"),
154            format!("{}\n", self.metadata.product),
155        )
156        .map_err(|e| UsbError::GadgetConfig(format!("Cannot write product: {}", e)))?;
157
158        let config_strings = base.join("configs/c.1/strings/0x409");
159        fs::write(config_strings.join("configuration"), "KOBOeReader\n")
160            .map_err(|e| UsbError::GadgetConfig(format!("Cannot write configuration: {}", e)))?;
161
162        let lun = base.join("functions/mass_storage.0/lun.0");
163        fs::write(lun.join("file"), format!("{}\n", self.metadata.partition))
164            .map_err(|e| UsbError::GadgetConfig(format!("Cannot write LUN file: {}", e)))?;
165
166        info!(
167            vendor_id = self.metadata.vendor_id,
168            product_id = self.metadata.product_id,
169            partition = %self.metadata.partition,
170            "Gadget configuration written"
171        );
172
173        Ok(())
174    }
175
176    /// Creates symlinks to activate the mass storage function.
177    fn activate_function(&self) -> Result<(), UsbError> {
178        debug!("Activating mass storage function");
179
180        let src = format!("{}/functions/mass_storage.0", CONFIGFS_GADGET_DIR);
181        let dst = format!("{}/configs/c.1/mass_storage.0", CONFIGFS_GADGET_DIR);
182
183        std::os::unix::fs::symlink(&src, &dst).map_err(|e| {
184            error!(source = %src, destination = %dst, error = %e, "Failed to create function symlink");
185            UsbError::GadgetConfig(format!("Cannot activate function: {}", e))
186        })?;
187
188        Ok(())
189    }
190
191    /// Binds the gadget to the UDC to enable USB.
192    ///
193    /// The UDC name is written with a trailing `\n` to match `echo` behaviour from
194    /// the kernel documentation examples.
195    fn bind_udc(&self) -> Result<(), UsbError> {
196        debug!(udc = %self.udc, "Binding to UDC");
197
198        let udc_path = format!("{}/UDC", CONFIGFS_GADGET_DIR);
199        fs::write(&udc_path, format!("{}\n", self.udc)).map_err(|e| {
200            error!(udc = %self.udc, error = %e, "Failed to bind UDC");
201            UsbError::Udc(format!("Cannot bind UDC: {}", e))
202        })?;
203
204        info!(udc = %self.udc, "USB gadget enabled");
205        Ok(())
206    }
207
208    /// Unbinds the gadget from the UDC.
209    ///
210    /// Writes a bare newline to the UDC attribute, which the kernel interprets
211    /// as clearing the binding.
212    fn unbind_udc(&self) -> Result<(), UsbError> {
213        debug!("Unbinding UDC");
214
215        let udc_path = format!("{}/UDC", CONFIGFS_GADGET_DIR);
216        fs::write(&udc_path, "\n").map_err(|e| {
217            error!(error = %e, "Failed to unbind UDC");
218            UsbError::Udc(format!("Cannot unbind UDC: {}", e))
219        })?;
220
221        info!("USB gadget disabled");
222        Ok(())
223    }
224
225    /// Removes the function symlink.
226    fn deactivate_function(&self) -> Result<(), UsbError> {
227        debug!("Deactivating mass storage function");
228
229        let link = format!("{}/configs/c.1/mass_storage.0", CONFIGFS_GADGET_DIR);
230        if Path::new(&link).exists() {
231            fs::remove_file(&link).map_err(|e| {
232                error!(path = %link, error = %e, "Failed to remove function symlink");
233                UsbError::GadgetConfig(format!("Cannot deactivate function: {}", e))
234            })?;
235        }
236
237        Ok(())
238    }
239
240    /// Removes the gadget directory structure.
241    fn remove_gadget_dirs(&self) -> Result<(), UsbError> {
242        debug!("Removing ConfigFS gadget directories");
243
244        let dirs = [
245            format!("{}/configs/c.1/strings/0x409", CONFIGFS_GADGET_DIR),
246            format!("{}/configs/c.1", CONFIGFS_GADGET_DIR),
247            format!("{}/functions/mass_storage.0/lun.0", CONFIGFS_GADGET_DIR),
248            format!("{}/functions/mass_storage.0", CONFIGFS_GADGET_DIR),
249            format!("{}/strings/0x409", CONFIGFS_GADGET_DIR),
250            CONFIGFS_GADGET_DIR.to_string(),
251        ];
252
253        for dir in &dirs {
254            if Path::new(dir).exists() {
255                if let Err(e) = fs::remove_dir(dir) {
256                    debug!(directory = %dir, error = %e, "Failed to remove directory (may be non-empty)");
257                }
258            }
259        }
260
261        Ok(())
262    }
263}
264
265impl KoboUsbOperations for MtkUsbManager {
266    fn metadata(&self) -> &DeviceMetadata {
267        &self.metadata
268    }
269}
270
271impl UsbManager for MtkUsbManager {
272    #[cfg_attr(feature = "otel", tracing::instrument(skip(self), ret(level=tracing::Level::TRACE)))]
273    fn enable(&self) -> Result<(), UsbError> {
274        info!("Enabling MTK USB mass storage");
275
276        self.prepare_for_usb_share()?;
277        self.create_gadget_dirs()?;
278        self.write_gadget_config()?;
279        self.activate_function()?;
280        self.bind_udc()?;
281
282        info!("MTK USB mass storage enabled successfully");
283        Ok(())
284    }
285
286    #[cfg_attr(feature = "otel", tracing::instrument(skip(self), ret(level=tracing::Level::TRACE)))]
287    fn disable(&self) -> Result<(), UsbError> {
288        info!("Disabling MTK USB mass storage");
289
290        self.unbind_udc()?;
291        self.deactivate_function()?;
292        self.remove_gadget_dirs()?;
293        self.check_filesystem()?;
294        self.remount_partitions()?;
295
296        info!("MTK USB mass storage disabled successfully");
297        Ok(())
298    }
299}