diff --git a/README.md b/README.md new file mode 100644 index 0000000..285f441 --- /dev/null +++ b/README.md @@ -0,0 +1,104 @@ +# AIC8800 USB Wi-Fi Driver for NixOS + +This flake provides the AIC8800 USB Wi-Fi driver (used by UGREEN AX900 and similar adapters) as a NixOS module. + +## Features + +- ✅ Kernel 6.17+ compatibility (includes fixes for timer and cfg80211 API changes) +- ✅ Automatic firmware installation +- ✅ Configurable udev rules for USB disk ejection and power management + +## Usage + +### In your flake.nix + +```nix +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + aic8800 = { + url = "git+https://git.tvpdev.de/Tsubajashi/alc8800"; + # Or use a local path: + # url = "path:/path/to/alc8800"; + }; + }; + + outputs = { self, nixpkgs, aic8800, ... }: { + nixosConfigurations.your-host = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + specialArgs = { + aic8800-src = aic8800; # Pass the flake source + }; + modules = [ + aic8800.nixosModules.default + ./configuration.nix + ]; + }; + }; +} +``` + +### In your configuration.nix + +```nix +{ + hardware.aic8800 = { + enable = true; + # Optional: enable udev rules (default: true) + enableUdevRules = true; + }; +} +``` + +### Alternative: Using as a flake input with specialArgs + +If you're already using `aic8800-src` as a flake input: + +```nix +{ + inputs = { + aic8800-src = { + url = "git+https://git.tvpdev.de/Tsubajashi/alc8800"; + flake = false; + }; + aic8800 = { + url = "git+https://git.tvpdev.de/Tsubajashi/alc8800"; + }; + }; + + outputs = { self, nixpkgs, aic8800, aic8800-src, ... }: { + nixosConfigurations.your-host = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + specialArgs = { + aic8800-src = aic8800-src; # Use the flake = false input + }; + modules = [ + aic8800.nixosModules.default + ./configuration.nix + ]; + }; + }; +} +``` + +## What it does + +When enabled, the module: + +1. **Builds the kernel modules** (`aic_load_fw` and `aic8800_fdrv`) for your kernel version +2. **Installs firmware** to `/lib/firmware/aic8800D80/` +3. **Loads the modules** automatically at boot +4. **Configures udev rules** to: + - Automatically eject USB storage devices that appear when the adapter is plugged in (switches to Wi-Fi mode) + - Keep the Wi-Fi interface awake to avoid autosuspend issues + +## Kernel Compatibility + +The driver includes compatibility fixes for Linux kernel 6.17+: +- Timer API changes (`del_timer` → `timer_delete`) +- cfg80211 API changes (additional parameter for spurious frame handlers) + +## Troubleshooting + +If you get an error about `aic8800-src` not being found, make sure you're passing it via `specialArgs` in your flake.nix. + diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..0a42f0e --- /dev/null +++ b/flake.nix @@ -0,0 +1,58 @@ +{ + description = "AIC8800 USB Wi-Fi driver for Linux"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + }; + + outputs = { self, nixpkgs }: + let + supportedSystems = [ "x86_64-linux" "aarch64-linux" ]; + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + in + { + packages = forAllSystems (system: + let + pkgs = import nixpkgs { inherit system; }; + in + { + aic8800-firmware = pkgs.stdenvNoCC.mkDerivation { + pname = "aic8800-firmware"; + version = "2022-12-19"; + src = ./.; + + installPhase = '' + runHook preInstall + mkdir -p $out/lib/firmware + cp -r usr/src/AIC8800/fw/aic8800D80 $out/lib/firmware/ + runHook postInstall + ''; + + meta = with pkgs.lib; { + description = "Firmware blobs for AIC8800 USB adapters"; + license = licenses.unfreeRedistributableFirmware; + platforms = platforms.linux; + }; + }; + } + ); + + nixosModules = { + default = { config, lib, pkgs, ... }: import ./nixos-module.nix { + inherit config lib pkgs; + aic8800-src = self; + }; + aic8800 = { config, lib, pkgs, ... }: import ./nixos-module.nix { + inherit config lib pkgs; + aic8800-src = self; + }; + }; + + overlays = { + default = final: prev: { + aic8800-firmware = self.packages.${prev.system}.aic8800-firmware; + }; + }; + }; +} + diff --git a/nixos-module.nix b/nixos-module.nix new file mode 100644 index 0000000..072b468 --- /dev/null +++ b/nixos-module.nix @@ -0,0 +1,153 @@ +{ config, lib, pkgs, aic8800-src ? null, ... }: + +with lib; + +let + cfg = config.hardware.aic8800; + + # Get source from function argument, specialArgs, or throw error + aic8800Src = if aic8800-src != null then + "${aic8800-src}/usr/src/AIC8800" + else if builtins.hasAttr "_module" config && + builtins.hasAttr "args" config._module && + builtins.hasAttr "aic8800-src" config._module.args then + "${config._module.args.aic8800-src}/usr/src/AIC8800" + else + throw '' + aic8800-src must be provided either: + 1. As a function argument when importing the module, or + 2. Via specialArgs in your flake.nix: + specialArgs = { aic8800-src = inputs.aic8800-src; }; + ''; + + aic8800Firmware = pkgs.stdenvNoCC.mkDerivation { + pname = "aic8800-firmware"; + version = "2022-12-19"; + src = aic8800Src; + + installPhase = '' + runHook preInstall + mkdir -p $out/lib/firmware + cp -r fw/aic8800D80 $out/lib/firmware/ + runHook postInstall + ''; + + meta = with lib; { + description = "Firmware blobs for AIC8800 USB adapters"; + license = licenses.unfreeRedistributableFirmware; + platforms = platforms.linux; + }; + }; + + aic8800Driver = config.boot.kernelPackages.callPackage + ({ stdenv, + lib, + kernel, + python3 + }: stdenv.mkDerivation { + pname = "aic8800-driver"; + version = "2022-12-19"; + + src = aic8800Src; + + nativeBuildInputs = (kernel.moduleBuildDependencies or [ ]) ++ [ python3 ]; + + postPatch = '' + python3 <<'EOF' + from pathlib import Path + + header = Path('drivers/aic8800/aic8800_fdrv/aicwf_usb.h') + lines = header.read_text().splitlines() + + define_line = '#define USB_PRODUCT_ID_AIC8800D80 0x8d80' + if all('USB_PRODUCT_ID_AIC8800D80' not in line for line in lines): + out_lines = [] + inserted = 0 + for line in lines: + out_lines.append(line) + if line.strip().startswith('#define USB_PRODUCT_ID_AIC8800D81'): + out_lines.append(define_line) + inserted += 1 + if inserted == 0: + raise SystemExit('marker for USB product IDs missing') + header.write_text('\n'.join(out_lines) + '\n') + + table = Path('drivers/aic8800/aic8800_fdrv/aicwf_usb.c') + table_text = table.read_text() + table_anchor = '{USB_DEVICE_AND_INTERFACE_INFO(USB_VENDOR_ID_AIC, USB_PRODUCT_ID_AIC8800D81, 0xff, 0xff, 0xff)},\n' + table_entry = '{USB_DEVICE_AND_INTERFACE_INFO(USB_VENDOR_ID_AIC, USB_PRODUCT_ID_AIC8800D80, 0xff, 0xff, 0xff)},\n' + + if table_entry not in table_text: + if table_anchor not in table_text: + raise SystemExit('USB table anchor missing') + table.write_text(table_text.replace(table_anchor, table_anchor + table_entry, 1)) + EOF + ''; + + hardeningDisable = [ "pic" "format" ]; + + buildPhase = '' + runHook preBuild + make -C drivers/aic8800 \ + KDIR=${kernel.dev}/lib/modules/${kernel.modDirVersion}/build \ + ARCH=${stdenv.hostPlatform.linuxArch} + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + modDir=$out/lib/modules/${kernel.modDirVersion}/kernel/drivers/net/wireless + install -Dm644 drivers/aic8800/aic_load_fw/aic_load_fw.ko \ + "$modDir/aic_load_fw.ko" + install -Dm644 drivers/aic8800/aic8800_fdrv/aic8800_fdrv.ko \ + "$modDir/aic8800_fdrv.ko" + mkdir -p $out/lib/firmware + cp -r --no-preserve=mode,ownership fw/aic8800D80 $out/lib/firmware/ + runHook postInstall + ''; + + meta = with lib; { + description = "UGREEN / AIC8800 USB Wi-Fi driver"; + homepage = "https://www.ugreen.com"; + license = licenses.unfreeRedistributableFirmware; + platforms = platforms.linux; + }; + }) { inherit (pkgs) python3; }; + +in +{ + options.hardware.aic8800 = { + enable = mkEnableOption "AIC8800 USB Wi-Fi driver support"; + + enableUdevRules = mkOption { + type = types.bool; + default = true; + description = "Enable udev rules for automatic USB disk ejection and power management"; + }; + }; + + config = mkIf cfg.enable { + boot.extraModulePackages = [ aic8800Driver ]; + boot.kernelModules = lib.mkAfter [ "aic_load_fw" "aic8800_fdrv" ]; + + hardware.firmware = [ aic8800Firmware ]; + + services.udev.extraRules = mkIf cfg.enableUdevRules (lib.mkAfter '' + # UGREEN AX900 shows up as a tiny storage device; ejecting it flips to Wi-Fi mode + KERNEL=="sd*", ACTION=="add", SUBSYSTEM=="block", \ + ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="5721", \ + SYMLINK+="aicudisk", RUN+="${pkgs.util-linux}/bin/eject /dev/%k" + KERNEL=="sd*", ACTION=="add", SUBSYSTEM=="block", \ + ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="5723", \ + SYMLINK+="tendaudisk", RUN+="${pkgs.util-linux}/bin/eject /dev/%k" + KERNEL=="sd*", ACTION=="add", SUBSYSTEM=="block", \ + ATTRS{idVendor}=="a69c", ATTRS{idProduct}=="5724", \ + SYMLINK+="ugreenudisk", RUN+="${pkgs.util-linux}/bin/eject /dev/%k" + + # Keep the Wi-Fi interface awake to avoid round-trip spikes after autosuspend + ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="a69c", ATTR{idProduct}=="8d80", \ + TEST=="power/control", ATTR{power/control}="on" + ''); + }; +} +