mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-17 05:13:43 +08:00
feat: auto-detect DJI 4G modules (#32)
This commit is contained in:
@@ -11,7 +11,11 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const quectelVendorID = "2c7c"
|
||||
const (
|
||||
quectelVendorID = "2c7c"
|
||||
djiVendorID = "2ca3"
|
||||
dji4GProductID = "4006"
|
||||
)
|
||||
|
||||
type SysFSDiscoverer struct {
|
||||
SysRoot string
|
||||
@@ -70,13 +74,13 @@ func (d *SysFSDiscoverer) Discover(ctx context.Context) ([]Candidate, error) {
|
||||
resolvedDevice = devicePath
|
||||
}
|
||||
vendorID := strings.ToLower(readTrimmed(filepath.Join(resolvedDevice, "idVendor")))
|
||||
if vendorID != quectelVendorID {
|
||||
productID := strings.ToLower(readTrimmed(filepath.Join(resolvedDevice, "idProduct")))
|
||||
if !isSupportedUSBModem(vendorID, productID) {
|
||||
continue
|
||||
}
|
||||
|
||||
state := devices[deviceName]
|
||||
if state == nil {
|
||||
productID := strings.ToLower(readTrimmed(filepath.Join(resolvedDevice, "idProduct")))
|
||||
serialNumber := readTrimmed(filepath.Join(resolvedDevice, "serial"))
|
||||
state = &discoveredUSBDevice{
|
||||
candidate: Candidate{
|
||||
@@ -141,6 +145,19 @@ func (d *SysFSDiscoverer) Discover(ctx context.Context) ([]Candidate, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func isSupportedUSBModem(vendorID, productID string) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(vendorID), quectelVendorID) ||
|
||||
IsDJI4GUSB(vendorID, productID)
|
||||
}
|
||||
|
||||
// IsDJI4GUSB reports whether a USB identity belongs to the first-generation
|
||||
// DJI/Baiwang 4G module. It keeps the factory 2ca3:4006 identity usable without
|
||||
// requiring a persistent AT+QCFG USB identity rewrite to Quectel 2c7c:0125.
|
||||
func IsDJI4GUSB(vendorID, productID string) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(vendorID), djiVendorID) &&
|
||||
strings.EqualFold(strings.TrimSpace(productID), dji4GProductID)
|
||||
}
|
||||
|
||||
type discoveredWWANDevice struct {
|
||||
index string
|
||||
ports []Port
|
||||
|
||||
@@ -2,6 +2,29 @@ package modem
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSupportedUSBModemIdentity(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
vendorID string
|
||||
productID string
|
||||
want bool
|
||||
}{
|
||||
{name: "Quectel", vendorID: "2c7c", productID: "0125", want: true},
|
||||
{name: "DJI 4G module", vendorID: "2ca3", productID: "4006", want: true},
|
||||
{name: "DJI 4G module uppercase", vendorID: "2CA3", productID: "4006", want: true},
|
||||
{name: "unrelated DJI device", vendorID: "2ca3", productID: "001f", want: false},
|
||||
{name: "unrelated USB device", vendorID: "0403", productID: "6001", want: false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if got := isSupportedUSBModem(test.vendorID, test.productID); got != test.want {
|
||||
t.Fatalf("isSupportedUSBModem(%q, %q) = %v, want %v", test.vendorID, test.productID, got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectATPortPrefersTTYUSB2AcrossUSBCompositions(t *testing.T) {
|
||||
ports := []Port{
|
||||
{Name: "ttyUSB2", InterfaceNumber: 0x02, Role: PortRoleDiagnostic},
|
||||
|
||||
@@ -387,6 +387,7 @@ func (s *Server) handleDiscoveredDevices(w http.ResponseWriter, r *http.Request)
|
||||
result = append(result, map[string]any{
|
||||
"hardware_kind": candidate.HardwareKind,
|
||||
"reader_name": candidate.ReaderName,
|
||||
"device_type": discoveredDeviceType(candidate),
|
||||
"discovery_key": entry.ID,
|
||||
"control_path": controlPath,
|
||||
"net_interface": candidate.NetworkInterface,
|
||||
@@ -1913,6 +1914,8 @@ func fillConfigFromPhysical(config *store.Device, entry device.Device) {
|
||||
config.NetworkEnabled = false
|
||||
config.SMSEnabled = true
|
||||
config.VoWiFiEnabled = true
|
||||
} else if modem.IsDJI4GUSB(candidate.VendorID, candidate.ProductID) {
|
||||
config.DeviceType = store.DeviceTypeDJI4G
|
||||
}
|
||||
if config.Interface == "" {
|
||||
config.Interface = candidate.NetworkInterface
|
||||
@@ -1937,6 +1940,16 @@ func fillConfigFromPhysical(config *store.Device, entry device.Device) {
|
||||
}
|
||||
}
|
||||
|
||||
func discoveredDeviceType(candidate modem.Candidate) string {
|
||||
if candidate.HardwareKind == "pcsc" {
|
||||
return store.DeviceTypeUSBSIMReader
|
||||
}
|
||||
if modem.IsDJI4GUSB(candidate.VendorID, candidate.ProductID) {
|
||||
return store.DeviceTypeDJI4G
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func modemSummary(snapshot *device.Snapshot, phone string, phoneSource string) map[string]any {
|
||||
if snapshot == nil {
|
||||
return map[string]any{
|
||||
|
||||
@@ -6,10 +6,28 @@ import (
|
||||
"time"
|
||||
|
||||
"vocat/internal/device"
|
||||
"vocat/internal/modem"
|
||||
"vocat/internal/store"
|
||||
"vocat/internal/vowifi"
|
||||
)
|
||||
|
||||
func TestFillConfigFromPhysicalClassifiesDJI4G(t *testing.T) {
|
||||
config := store.Device{DeviceType: store.DeviceTypePCIeEC20EC25}
|
||||
entry := device.Device{Candidate: modem.Candidate{
|
||||
VendorID: "2ca3",
|
||||
ProductID: "4006",
|
||||
}}
|
||||
|
||||
fillConfigFromPhysical(&config, entry)
|
||||
|
||||
if config.DeviceType != store.DeviceTypeDJI4G {
|
||||
t.Fatalf("device type = %q, want %q", config.DeviceType, store.DeviceTypeDJI4G)
|
||||
}
|
||||
if got := discoveredDeviceType(entry.Candidate); got != store.DeviceTypeDJI4G {
|
||||
t.Fatalf("discovered device type = %q, want %q", got, store.DeviceTypeDJI4G)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfiguredDeviceSummaryIgnoresVoWiFiRuntimeFromPreviousSIM(t *testing.T) {
|
||||
database, err := store.Open(context.Background(), ":memory:")
|
||||
if err != nil {
|
||||
|
||||
@@ -398,7 +398,7 @@ export default function DevicesPage() {
|
||||
modemImei: d.imei || "",
|
||||
usbPath: d.usbPath || "",
|
||||
deviceBackend: backend,
|
||||
deviceType: isReader ? "usb_sim_reader" : prev.deviceType,
|
||||
deviceType: d.deviceType || (isReader ? "usb_sim_reader" : prev.deviceType),
|
||||
esimTransport: isReader ? "pcsc" : backend,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -174,6 +174,7 @@ export interface DeviceStatus {
|
||||
export interface DiscoveredDevice {
|
||||
hardwareKind?: string;
|
||||
readerName?: string;
|
||||
deviceType?: DeviceType;
|
||||
discoveryKey: string;
|
||||
controlPath: string;
|
||||
netInterface: string;
|
||||
|
||||
Reference in New Issue
Block a user