たくさんの自由帳

Android で AT コマンドを叩いただけ

投稿日 : | 0 日前

文字数(だいたい) : 3507

この辺の技術はほとんどがクローズドソースというか、仕様が公開されていないので、
結局何が出来るのかよく分からずあんまりおもしろくなかった。

本題

root 権限が必要です。自己責任で。

あぷり

https://github.com/takusan23/AndroidAtCommandUI/releases/tag/1.0.0

APKを作りました、が、AT コマンドを知っていないと何も出来ないです。
AT コマンド詳しい人がいれば(実在するの?)このアプリでいろいろ叩いてみて遊べるかもしれません。

Imgur

起動直後はデバイスが/dev/smd0とかになってますが、ATコマンドの欄でコマンドを実行して応答がなければ/dev/smd7とかに変更してみてください。
テスト用コマンドはATです。OKが返ってくるはず。

おことわり

Qualcommしか知りません。(Google PixelとかはExynosなので!)

参考

https://stackoverflow.com/questions/8284067/

必要なもの

root化済みの端末

最難関ですね。修理出来ないので壊したら終わり
ところで、電波測定を最大限楽しむにはSIM フリーじゃなくてキャリアで売られているスマホを買う必要が多分あります。 理由としては、

  • 5G SAに接続できるか怪しい
  • ドコモの転用5G(なんちゃって5G / NR 化)を掴むためにはドコモで買った端末が必要(IMEIをチェックしているらしい)
    • au / softbankは普通にキャリア以外の端末で転用5Gつながる

しかし、キャリアで買ったスマホはroot化が(ほぼ)出来ない状態です。壊れるかもしれない(一向に起動アニメーションが終わらない等)ので塞がれて正解ですが。。。

Android 4.xくらいの時までは盛り上がっていたはずで、(Nexus 7いじるの楽しかったなあ)
SuperuserSuperSU、あとはKingRootとかいう怪しい雰囲気のやつとかありましたね、懐かしい

Xperia Z3くらいでroot化の文化が途絶えてしまったわけですが、Samsungとかは今でもできるらしい?おサイフケータイだかNFCが4ぬらしいけど。
キャリアで売られてるGoogle Pixelはどうかなあ、昔はSIMロック解除さえすればBootloaderがアンロック出来たはず?
https://takusan.negitoro.dev/posts/android_12_dp_hitobashira/

なので、電波測定を犠牲にしてrootが必要ならSIMフリーを買うしかない。SIMフリーとして売られてる端末を買えばroot行けるはず。
Xperia もSIMフリー買えば今でもできるらしい?高い金払って壊れる可能性があるものに価値を見いだせるかというと、、、

ADB がインストールされたパソコン

Android Studio入れたら付いてくるっしょ
多分adb単品も行けるはず?platform-toolsだけ入れる方法があった気がする

とりあえず叩いてみる

先述の通りAndroid端末をroot化、もといsuコマンドが叩ける状態にする必要があります。

コマンドプロンプトでもGitBashでもなんでも良いのでターミナルさんを開いてください。
adb shellした後suすると、rootユーザーに昇格できます。$(ドルマーク)だったのが#(シャープ、いげた)に変化するはず。

C:\Users\takusan23>adb shell
OnePlus7TPro:/ $ su
OnePlus7TPro:/ #

Imgur

あ、Magiskに許可するか聞かれたら許可してあげてください。

Imgur

AT コマンドを叩く

にはもう一個手間が必要で、AT コマンドを叩く先を探す必要があります。
というのも、端末によっては/dev/smd0だったり、/dev/smd7だったりと違うみたいなんですよね。

とりあえずこれを叩いてみます。最後の\rが必要です。
成功すればOKという文字が出ます。

echo -e "AT\r" > /dev/smd7 && cat /dev/smd7

どうでしょうか。
ATの後にOKがでましたか?出ていればAT コマンドを叩ける状態です。
AT\rの部分を好きなAT コマンドにすれば良いのですが、クローズドソースというか仕様が公開されていないため、何を叩けば良いのかは調べるしかないです。

AT
OK

ちなみにCtrl+Cで抜けることが出来ます(操作ができるようになります)

ダメだった場合

/dev/smd7の部分を/dev/smd0とかにすれば良いのですが、まずsmdが何種類あるのか見てみましょう。
見つかったもの全てにAT\rを投げてOKが返ってくるものがあればそれを使えば良いはずです。

以下のコマンドを叩きます

ls /dev/smd*

すると、こんな感じにsmd7とかが一覧で表示されるはず。(以下省略)

/dev/smd0  /dev/smd7 

あとは出てきたsmdに対してAT\rを投げていってOKが出てくるまで繰り返せばおけ。
/dev/smd0の部分を置き換えていく)

echo -e "AT\r" > /dev/smd0 && cat /dev/smd0

色々叩いてみる

端末の情報

ATIを叩くと端末の情報が取れるそうです。

echo -e "ATI\r" > /dev/smd7 && cat /dev/smd7

適当に置き換えてますがこんな感じ

OnePlus7TPro:/ # echo -e "ATI\r" > /dev/smd7 && cat /dev/smd7
ATI
Manufacturer: QUALCOMM INCORPORATED
Model: 0000
Revision: xxxxx
SVN: 00
IMEI: 00000
+GCAP: +CGSM
 
OK

ATコマンド一覧を表示

AT+CLACを叩くと表示できるそうです。
端末によっては塞がれている場合があるそうです。

OnePlus7TPro:/ # echo -e "AT+CLAC\r" > /dev/smd7 && cat /dev/smd7
AT+CLAC
&C
&D
&E
&F
&S
&V
&W
以下省略...

EARFCN 取得(うまくいかなかった)

AT$QRSRPを叩くと、EARFCN(バンド)が取れるらしい、、、ですがうまく行きませんでした。
OKが帰ってきますが何もでてきません。

OnePlus7TPro:/ # echo -e "AT$QRSRP\r" > /dev/smd7 && cat /dev/smd7
AT
OK

基地局を探す

AT+COPS=?を叩くと、しばらく待った後に近くの基地局を返してくれるそうです。
先述の通りOKが表示されるまで、少し時間がかかります。

全部見せて良いのか知らんから隠すわ。

OnePlus7TPro:/ # echo -e "AT+COPS=?\r" > /dev/smd7 && cat /dev/smd7
AT+COPS=?
+COPS: (3,"JP DOCOMO","DOCOMO","44010",2) 以下省略
 
OK

選択したネットワークを取得

AT+COPS?で接続中のが見れるらしい。確かにLINEMOなのであっていそう

OnePlus7TPro:/ # echo -e "AT+COPS?\r" > /dev/smd7 && cat /dev/smd7
AT+COPS?
+COPS: 0,0,"SoftBank LINEMO",7
 
OK

AT コマンドを叩く Android アプリを作る

root 権限でコマンドを叩く

https://stackoverflow.com/questions/5484535/

pingコマンドとかはRuntime.getRuntime().exec()で良いのですが、root権限が(suする)必要な場合。
su -cした後にコマンドを入れればいいらしいです。

su -c この後にコマンド

最小限の実装例です。
文字列で渡すのではなく、arrayOfで一つ一つ分解するのが良いらしいです。

lifecycleScope.launch(Dispatchers.IO) {
    val command = arrayOf("su", "-c", "whoami")
    val process = Runtime.getRuntime().exec(command)
    process.inputStream.bufferedReader().forEachLine { readLine ->
        // 出力を logcat に
        println(readLine)
    }
}

Magiskで許可するか聞かれるので許可してあげてください。

Imgur

適当に UI を作る

最低限で。
cat /dev/smd7をループで舐めるやつと、コマンドを投げるやつを分けてみた。
けどなんかうまく動いてない時がある。。。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
 
        setContent {
            AndroidAtCommandUITheme {
                MainScreen()
            }
        }
    }
}
 
@Composable
private fun MainScreen() {
    val scope = rememberCoroutineScope()
    val outputList = remember { mutableStateOf(emptyList<String>()) }
    val commandText = remember { mutableStateOf("AT") }
 
    /** AT コマンドを投げる */
    fun executeCommand() {
        scope.launch(Dispatchers.IO) {
            // 出力は outputList で
            Runtime.getRuntime().exec(
                arrayOf("su", "-c", "echo", "-e", """ "${commandText.value}\r" """, ">", "/dev/smd7")
            )
        }
    }
 
    // AT コマンドの出力を while ループで取り出す
    LaunchedEffect(key1 = Unit) {
        withContext(Dispatchers.IO) {
            val process = Runtime.getRuntime().exec(arrayOf("su", "-c", "cat", "/dev/smd7"))
            launch {
                // 出力を取り出す
                try {
                    process.inputStream.bufferedReader().use { bufferedReader ->
                        while (isActive) {
                            val readText = bufferedReader.readLine()?.ifEmpty { null } ?: continue
                            outputList.value += readText
                        }
                    }
                } catch (e: Exception) {
                    // 握りつぶす
                }
            }
            // cat を終わらせる
            try {
                awaitCancellation()
            } finally {
                process.destroy()
            }
        }
    }
 
    Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
        Column(modifier = Modifier.padding(innerPadding)) {
 
            Row(
                modifier = Modifier.padding(10.dp),
                horizontalArrangement = Arrangement.spacedBy(5.dp),
                verticalAlignment = Alignment.CenterVertically
            ) {
                OutlinedTextField(
                    modifier = Modifier.weight(1f),
                    value = commandText.value,
                    onValueChange = { commandText.value = it },
                    singleLine = true,
                    keyboardOptions = KeyboardOptions(
                        imeAction = ImeAction.Go,
                        keyboardType = KeyboardType.Ascii
                    ),
                    keyboardActions = KeyboardActions(onGo = { executeCommand() }),
                    label = { Text(text = "AT コマンド") }
                )
 
                Button(onClick = { executeCommand() }) {
                    Text(text = "実行")
                }
            }
 
            LazyColumn {
                items(outputList.value) { output ->
                    Text(
                        modifier = Modifier.fillMaxWidth(),
                        text = output
                    )
                    HorizontalDivider(color = LocalContentColor.current.copy(alpha = 0.05f))
                }
            }
        }
    }
}

こんな感じです。
下に積まれていくのでスクロールしないといけない。

Imgur

ソースコード

https://github.com/takusan23/AndroidAtCommandUI

おわりに

AT コマンド、マジで情報がないので時間を無駄にした間が半端ない。
これならlogcat -b radioのログでも眺めてたほうがまだ有意義です。