On Tue, 2012-02-07 at 09:49 +0100, Bjørn Mork wrote: > Dan Williams <dcbw@xxxxxxxxxx> writes: > > On Wed, 2012-02-01 at 16:05 +0100, Bjørn Mork wrote: > >> Another version of the subdriver patch set for your review. > >> > >> Changes in v2: > >> - autoresume on open lock bugfix is integrated > >> - manage_power support (setting needs_remote_wakeup on a shared interface) for > >> subdriver is added > >> - two qmi_wwan patches have been included to illustrate how this is supposed > >> to be used, and to ease testing > >> > >> These patches should be applied on top of what's currently in linux-next. > > > > I've confirmed that this patchset works to talk QMI to the UML290 and > > Gobi 2000 devices I have. I haven't gotten to the point of trying an > > actual connect sequence (thus testing the net bits), but I can talk with > > the devices over QMI with cdc-wdmX. > > Thanks for testing. Good to know that the matching logic works. > > Now I'm quite confident that you are able to and want to write the > connection software, but if you or anyone else are interested in the > primitive script I'm using then here it is. This was intented for my > own use so it's quite Debian specific and not very fail safe. But it's > a start for anyone who wants to try the driver. > > Please no flames for the perl ;-) Hah, no problem. Thanks for posting it, otherwise I'd end up hacking up my tool for it too. One odd thing; my Gobi works just fine, but the UML290 appears to send responses for commands that aren't the command I sent to it. I'm not sure what's turning those unsolicited responses on, but I hadn't yet gotten around to having my test tool (which just creates a WDS client and tries to grab the MEID) wait for the appropriate response and ignore unsolicited ones. Anyway... > Yes, I know I should contribute to ModemManager instead, but I'll have > to learn some python first... It's actually written in C, but the decode stuff I wrote in python since it's much quicker for tools like that than C is. Kinda like perl. > BTW, you are probably already aware of all the necessary features of a > fullblown modem manager, but I just found one feature I've never needed > before: The ability to manually trigger a network search. The test > network I'm using supports handover from LTE to UMTS/GSM but not the > other way. This means that I often end up using UMTS even if LTE is > available when I've been moving through areas without LTE. > > With QMI the appropriate way to do this seems to be sending QMI_NAS > command 0x0033 (QMI_NAS_SET_SYSTEM_SELECTION_PREFERENCE) with TLV 0x11 > set to the exact same value it was before (which can be checked with > QMI_NAS command 0x0034). This immediately starts a new search without > changing any roaming or other network selection settings. And I end up > with LTE again. Interesting; there's also NAS/Perform Network Scan (command #33) which might work here. > The disadvantage of doing a network search is of course that any > connection will be lost. But until I found the QMI_NAS command I had to > unplug/replug the modem to move from UMTS to LTE so it's still an > improvement to me. > > I assume that my soecific problem is temporary and only due to the > current limited access to this LTE network. But the feature may still > be useful. Probably; once your provider and the device firmware support handup from UMTS to LTE it'll go away. Dan > > Bjørn > > plain text document attachment (qmi-ifupdown) > #!/usr/bin/perl > # > # /etc/network/if-pre-up.d/qmi > # /etc/network/if-post-down.d/qmi > # > # Copyright 2012 Bjørn Mork <bjorn@xxxxxxx> > # > # This script is an addition to Debian's ifupdown and establishes wwan > # connection using the Qualcomm propietary QMI protocol > # > # Place this file in /etc/network/if-pre-up.d/ and create a symlink to > # /etc/network/if-post-down.d/ > # > # The wwan interface is configured like this, where all variables are > # optional unless required by your subscription > # > # iface wwan0 inet dhcp > # wwan_pin "1234" > # wwan_apn "my.provider.apn" > # wwan_user "username" > # wwan_pw "password" > # # enable script debugging > # wwan_debug 1 > > use strict; > use warnings; > use constant { > QMI_CTL => 0, > QMI_WDS => 1, > QMI_DMS => 2, > }; > > # output control > my $verbose = $ENV{'VERBOSITY'}; > my $debug = $ENV{'IF_WWAN_DEBUG'}; > > # this is required > my $netdev = $ENV{'IFACE'} || die "No IFACE environment variable\n"; > my $cmd; > if ($ENV{'PHASE'} eq 'pre-up') { > $cmd = 'start'; > } elsif ($ENV{'PHASE'} eq 'post-down') { > $cmd = 'stop'; > } else { > exit 0; > } > > # need to keep some external state > my $state = "/etc/network/run/qmistate.$netdev"; > > # per interface config > my $pin = &strip_quotes($ENV{'IF_WWAN_PIN'}); > my $apn = &strip_quotes($ENV{'IF_WWAN_APN'}); > my $user = &strip_quotes($ENV{'IF_WWAN_USER'}); > my $pw = &strip_quotes($ENV{'IF_WWAN_PW'}); > > # internal state > my $dev; # management device > my @cid; # array of allocated CIDs > my $tid = 1; # transaction id > my $wds_handle; # connection handle > > > sub strip_quotes { > my ($stripped) = $_[0] ? ($_[0] =~ /^"?([^"]*)"?$/) : (''); > return $stripped; > } > > sub debug_print { > my ($pfx, $packet) = @_; > printf STDERR $pfx . " %02x" x length($packet) . "\n", unpack("C*", $packet); > } > > # use the first cdc-wdmX dev on the same USB device as IFACE for mgmt > sub get_mgmt_dev { > my $ret = ''; > my $usbdev = readlink("/sys/class/net/$netdev/device"); > $usbdev =~ s!.*/([^:]*):.*!$1!; # ../../../2-1:1.4 > return $ret if (!$usbdev); > > opendir(D, "/sys/class/usb") || return $ret; > while (my $f = readdir(D)) { # cdc-wdm0 -> ../../devices/pci0000:00/0000:00:1d.7/usb2/2-1/2-1:1.3/usb/cdc-wdm0 > next unless ($f =~ /^cdc-wdm/); > if (readlink("/sys/class/usb/$f") =~ m!/$usbdev/$usbdev:.*/usb/cdc-wdm!) { # found it! > $ret = "/dev/$f"; > last; > } > } > closedir(D); > return $ret; > } > > > ### QMI helpers ### > # $tlvs = { type1 => packdata, type2 => packdata, .. > sub mk_qmi { > my ($sys, $cid, $msgid, $tlvs) = @_; > > # create tlvbytes > my $tlvbytes = ''; > foreach my $tlv (keys %$tlvs) { > $tlvbytes .= pack("Cv", $tlv, length($tlvs->{$tlv})) . $tlvs->{$tlv}; > } > > # create message > my $tlvlen = length($tlvbytes); > if ($sys != QMI_CTL) { > return pack("CvCCCCvvv", 1, 12 + $tlvlen, 0, $sys, $cid, 0, $tid++, $msgid, $tlvlen) . $tlvbytes; > } else { > return pack("CvCCCCCvv", 1, 11 + $tlvlen, 0, QMI_CTL, 0, 0, $tid++, $msgid, $tlvlen) . $tlvbytes; > } > } > > # decode packet into hash > sub decode_qmi { > my $packet = shift || return {}; > my $ret = {}; > @$ret{'tf','len','ctrl','sys','cid'} = unpack("CvCCC", $packet); > > # sanity check > return {} unless ($ret->{tf} == 1); > > # tid is 1 byte for QMI_CTL and 2 bytes for the others... > @$ret{'flags','tid','msgid','tlvlen'} = unpack($ret->{sys} == QMI_CTL ? "CCvv" : "Cvvv" , substr($packet, 6)); > my $tlvlen = $ret->{'tlvlen'}; > my $tlvs = substr($packet, $ret->{'sys'} == QMI_CTL ? 12 : 13 ); > > # unpack the tlvs > while ($tlvlen > 0) { > my ($tlv, $len) = unpack("Cv", $tlvs); > $ret->{'tlvs'}{$tlv} = [ unpack("C*", substr($tlvs, 3, $len)) ]; > $tlvlen -= $len + 3; > $tlvs = substr($tlvs, $len + 3); > } > return $ret; > } > > # check if two QMI messages are part of the same transaction > sub qmi_match { > my ($q1, $q2) = @_; > > for my $f (qw(tf ctrl flags sys cid msgid)) { > return undef unless (exists($q1->{$f}) && exists($q2->{$f}) && $q1->{$f} == $q2->{$f}); > } > return 1; > } > > # read from the already open F until match or timeout > sub read_match { > my $match = shift; > my $timeout = shift; > > my $qmi_in = {}; > eval { > local $SIG{ALRM} = sub { die "alarm\n" }; # NB: \n required > my $raw; > my $found; > alarm $timeout; > do { > if (!$raw) { > my $len = sysread(F, $raw, 256); > $found = 1 if !$len; # break out of loop in signal > &debug_print("<<", $raw) if $debug; > } > > $qmi_in = &decode_qmi($raw); > > # a single read may return more than one packet! > if ($qmi_in->{tf}) { > $raw = substr($raw, $qmi_in->{len} + 1); > } else { > $raw = ''; > } > > # matching reply? > $found ||= &qmi_match($match, $qmi_in); > } while (!$found); > alarm 0; > }; > if ($@) { > die unless $@ eq "alarm\n"; # propagate unexpected errors > } > return $qmi_in; > } > > # send $cmd to the already open F and wait for a reply > sub send_and_recv { > my $cmd = shift; > my $timeout = shift || 5; > return {} unless $cmd; > > &debug_print(">>", $cmd) if $debug; > print F $cmd; > > # set up for matching > my $qmi_out = &decode_qmi($cmd); > $qmi_out->{flags} = $qmi_out->{sys} ? 0x02 : 0x01; # response > $qmi_out->{ctrl} = 0x80; # service > my $qmi_in = &read_match($qmi_out, $timeout); > > return $qmi_in; > } > > # get status TLV error code > sub verify_status { > my $qmi = shift; > return 1 if ((ref($qmi) ne "HASH") || !exists($qmi->{tf})); # QMI_ERR_MALFORMED_MSG > return 0 if (!exists($qmi->{tlvs}) || !exists($qmi->{tlvs}{0x02})); # QMI_ERR_NONE > return unpack("v", pack("C*", @{$qmi->{tlvs}{0x02}}[2..3])); > } > > > ## QMI_CTL commands > > # allocate a client ID for $sys > sub get_cid { > my $sys = shift; > return $cid[$sys] if $cid[$sys]; > > # QMI_CTL request client ID > my $req = &mk_qmi(QMI_CTL, 0, 0x0022, {0x01 => pack("C*", $sys)}); > > restart: > my $ret = &send_and_recv($req); > my $status = &verify_status($ret); > if (!$status && $ret->{tlvs}{0x01}[0] == $sys) { > $cid[$sys] = $ret->{tlvs}{0x01}[1]; > } else { > if ($status == 0x0005) { # QMI_ERR_CLIENT_IDS_EXHAUSTED > if (!&ctl_sync) { # reset to clean state > goto restart; > } > } > warn "$netdev: CID request for sys=$sys failed: $status\n"; > } > return $cid[$sys]; > } > > # release all CIDs with the possible exception of QMI_WDS if we started a connection > sub release_cids { > for (my $sys = 0; $sys < scalar @cid; $sys++) { > if ($cid[$sys]) { > if ($wds_handle && $sys == QMI_WDS) { > warn "$netdev: not releasing QMI_WDS cid=$cid[$sys] while connected\n" if $verbose; > next; > } > my $req = &mk_qmi(QMI_CTL, 0, 0x0023, {0x01 => pack("C*", $sys, $cid[$sys])}); > my $ret = &send_and_recv($req); > printf STDERR "$netdev: released sys=$sys cid=$cid[$sys] with status=0x%04x\n", &verify_status($ret) if $verbose; > $cid[$sys] = 0; > } > } > } > > # sending a QMI_CTL SYNC message will release all allocated CIDs and > # therefore also disconnect device! > sub ctl_sync { > my $req = &mk_qmi(QMI_CTL, 0, 0x0027); > my $ret = &send_and_recv($req); > my $status = &verify_status($ret); > if (!$status) { > # reset all cached state as it is now invalid > @cid = (); > $wds_handle = 0; > } > return $status; > } > > # will receive a QMI_CTL sync notification when device is ready after > # PIN verification > sub wait_for_sync_ind { > my $timeout = shift || 5; > > # set up for matching > my $match = { > tf => 1, > sys => 0, > cid => 0, > flags => 0x02, > ctrl => 0x80, > msgid => 0x0027, > }; > my $qmi_in = &read_match($match, $timeout); > > return exists($qmi_in->{tf}); > } > > sub mk_wds { > my $cid = &get_cid(QMI_WDS); > return undef if (!$cid); > return &mk_qmi(QMI_WDS, $cid, @_); > } > > sub mk_dms { > my $cid = &get_cid(QMI_DMS); > return undef if (!$cid); > return &mk_qmi(QMI_DMS, $cid, @_); > } > > ## QMI_WDS commands > > # QMI_WDS 0x0020 > sub wds_start_network_interface { > # check PIN status first > if (!&dms_verify_pin) { > warn "$netdev: cannot connect without PIN verification\n"; > return 1; > } > > my %tlv; > $tlv{0x14} = $apn if $apn; > $tlv{0x17} = $user if $user; > $tlv{0x18} = $pw if $pw; > my $req = mk_wds(0x0020, \%tlv); # QMI_WDS_START_NETWORK_INTERFACE > > warn "$netdev: connecting...\n" if $verbose; > # need to save handle (and WMS CID!!!) for disconnect > my $ret = &send_and_recv($req, 60); > my $status = &verify_status($ret); > if ($status) { > my $v = $ret->{tlvs}{0x11}; # Verbose Call End Reason > if ($v) { > printf STDERR "$netdev: connection failed - status=0x%04x, type=0x%04x, reason=%04x\n", $status, unpack("v2", pack("C*", @$v)); > } else { > printf STDERR "$netdev: connection failed - status=0x%04x\n", $status; > } > return $status; > } > > my $v = $ret->{tlvs}{0x01}; > $wds_handle = unpack("V*", pack("C*", @$v)); # save as a 32bit integer > printf STDERR "$netdev: got QMI_WDS handle 0x%08x\n", $wds_handle if $verbose; > > return $status; > } > > # QMI_WDS 0x0021 > sub wds_stop_network_interface { > return 1 if !$wds_handle; # cannot disconnect without a valid handle > > my $req = mk_wds(0x0021, { 0x01 => pack("V", $wds_handle) } ); # QMI_WDS_STOP_NETWORK_INTERFACE > my $ret = &send_and_recv($req); > $wds_handle = 0; # reset handle to allow releasing the CID > return &verify_status($ret); > } > > # QMI_WDS 0x0022 > sub wds_get_pkt_srvc_status { > my $ret = &send_and_recv(&mk_wds(0x0022), 2); # QMI_WDS_GET_PKT_SRVC_STATUS, short timeout > my $status = verify_status($ret); > if ($status) { > warn "$netdev: wds_get_pkt_srvc_status: $status\n" if $verbose; > return 0; > } > my $v = $ret->{tlvs}{0x01}; > return $v ? $v->[0] : 0; > } > > ## QMI_DMS commands > > # QMI_DMS 0x0023 > sub dms_get_device_rev_id { > my $ret = &send_and_recv(&mk_dms(0x0023)); # QMI_DMS_GET_DEVICE_REV_ID > my $v = $ret->{tlvs}{0x01}; > return '' if (!$v); > return pack("C*", @$v); > } > > > # QMI_DMS 0x0028 > sub dms_enter_pin { > unless ($pin) { > warn "$netdev: No PIN configured\n" if $verbose; > return undef; > } > > my $req = &mk_dms(0x0028, # QMI_DMS_UIM_VERIFY_PIN > { 0x01 => pack("C*", 1, length($pin)) . $pin}); > > my $ret = &send_and_recv($req); > my $status = &verify_status($ret); > if ($status) { > warn "$netdev: PIN verification failed: $status\n"; > return undef; > } > > # wait for device to be ready > &wait_for_sync_ind(20); > > return 1; > } > > # QMI_DMS 0x002b - get SIM PIN status > sub dms_verify_pin { > my $ret = &send_and_recv(&mk_dms(0x002b)); # QMI_DMS_UIM_GET_PIN_STATUS > my $status = &verify_status($ret); > if ($status) { > warn "$netdev: PIN verfication failed: $status\n"; > warn "$netdev: SIM card missing?\n" if ($status == 0x0003); # QMI_ERR_INTERNAL > return undef; > } > > my $tlv = $ret->{tlvs}{0x11}; # PIN1 (SIM PIN) status > warn "$netdev: PIN1 status: $tlv->[0], verify_left: $tlv->[1], unblock_left: $tlv->[2]\n" if $verbose; > return 1 if ($tlv->[0] == 2 || $tlv->[0] == 3); # "enabled, verified" or "disabled" > > if ($tlv->[0] == 1) { # "enabled, not verified" > if ($tlv->[1] >= 3) { # requiring at least 3 remaining attempts > return &dms_enter_pin; > } else { > warn "$netdev: less than 3 verification attempts left for PIN1 - must be entered manually!\n" if $verbose; > } > } > return undef; > } > > ## external state management > > sub save_wds_state { > if (open(X, ">$state")) { > if ($wds_handle) { > printf X "%u %u\n", $cid[QMI_WDS], $wds_handle; > } > close X; > } else { > warn "$netdev: FATAL: cannot open \"$state\": $!\n"; > $wds_handle = 0; # will cause disconnect when CID is released > } > } > > sub read_wds_state { > if (!open(X, $state)) { # this is to be expected if we never saved any... > warn "$netdev: unable to open $state: $!\n" if $debug; > return; > } > my $x = <X>; > close X; > ($cid[QMI_WDS], $wds_handle) = split(/ /, $x) if $x; > > # verify that the state is valid > my $conn = &wds_get_pkt_srvc_status; > if ($conn != 2) { # CONNECTED; > $wds_handle = 0; # handle is invalid > } > if (!$conn) { > $cid[QMI_WDS] = 0; # CID is invalid > } > $wds_handle ||= 0; > printf STDERR "$netdev: QMI_WDS cid=%u, wds_handle=0x%08x\n", $cid[QMI_WDS], $wds_handle if $verbose; > } > > # restore sane state on exit > sub exit_proc { > &save_wds_state; # save state for next run > &release_cids; # release all releasable CIDs > close(F); # close device > } > > ## main > > # locate the (possibly QMI) management character device > $dev = &get_mgmt_dev || exit 0; > warn "$netdev: will use $dev for management\n" if $verbose; > > # open character device > open(F, "+<", $dev) || die "open $dev: $!\n"; > autoflush F 1; > > # at this point we'd like to ensure that CIDs are released on exit > $SIG{TERM} = \&exit_proc; > $SIG{INT} = \&exit_proc; > > # verify that it speaks QMI > my $revid = &dms_get_device_rev_id || exit 0; > warn "$netdev: revision: $revid\n" if $verbose; > > # get and verify cached data, so we can reuse the QMI_WDS CID at least > &read_wds_state; > > &wds_start_network_interface if ($cmd eq 'start'); > &wds_stop_network_interface if ($cmd eq 'stop'); > &exit_proc; > > -- To unsubscribe from this list: send the line "unsubscribe linux-usb" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html