Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Gradle
.gradle/
.kotlin/
build/
**/build/
local.properties
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ which bundles four companion services on one device:

| Feature | What it does |
|---|---|
| 🖥️ **Portable USB** | Find your GoConsoleOS USB host on Wi-Fi, launch it, open the USB Installer |
| 🖥️ **Portable USB** | Find your GoConsoleOS USB host on Wi-Fi and open the **Portable App** panel — a full launcher for host tools: GoUsbMaker, USB Health, Cast, GoAI, GoStore and Screenshot (`tools.list` / `tools.run`) |
| 📡 **GoConsoleOS Link** | LAN transport to browse and launch the host's game library over the same Wi-Fi (Steam Link style) |
| 🛡️ **USB Health** | Local on-device volume & health check plus the host's deep SMART report for portable USB drives (Android 13/16) |
| 📺 **GoConsoleOS Cast** | Screen-cast the device to the host / TV via the same transport |
Expand All @@ -32,6 +32,10 @@ UDP :39100 discovery GCS hello → hosts reply with a JSON beacon
TCP :39101 streaming control lines + length-prefixed binary frames (JPEG, USB, cast)
```

Control messages include `hello`, `games.list`, `games.launch`, `usb.list`,
`pair`, `cast.start`/`cast.stop`, and the Portable App panel's `tools.list`
(fetch the remote tool catalogue) and `tools.run` (run a tool by id).

See [docs/PROTOCOL.md](docs/PROTOCOL.md) for the wire format and
[docs/HOST.md](docs/HOST.md) for the Windows host service.

Expand Down
4 changes: 2 additions & 2 deletions apps/portable/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ android {
applicationId = "com.gostudios.console.portable"
minSdk = 23
targetSdk = 35
versionCode = 18
versionName = "1.1.0"
versionCode = 19
versionName = "1.2.0"
vectorDrawables { useSupportLibrary = true }
}

Expand Down
1 change: 1 addition & 0 deletions apps/portable/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
</activity>

<activity android:name=".LinkActivity" android:exported="false" android:screenOrientation="landscape" />
<activity android:name=".PortableActivity" android:exported="false" android:screenOrientation="landscape" />
<activity android:name=".UsbHealthActivity" android:exported="false" />
<activity android:name=".cast.MediaProjectionActivity" android:exported="false" />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ import com.gostudios.console.portable.cast.MediaProjectionActivity
import com.gostudios.console.sdk.ConsoleHost
import com.gostudios.console.sdk.DeviceServer
import com.gostudios.console.sdk.Discovery
import com.gostudios.console.sdk.LinkClient
import com.gostudios.console.sdk.Protocol
import org.json.JSONObject

/**
* Home screen of GoConsoleOS Portable. Discovers a GoConsoleOS host on the LAN
Expand Down Expand Up @@ -40,7 +37,7 @@ class MainActivity : AppCompatActivity() {
activeHost?.let { startActivity(MediaProjectionActivity.intent(this, it)) }
}
findViewById<LinearLayout>(R.id.btnBoot).setOnClickListener {
activeHost?.let { openUsbInstaller(it) }
activeHost?.let { startActivity(PortableActivity.intent(this, it)) }
}

scanner = Discovery.Scanner { host ->
Expand All @@ -58,21 +55,6 @@ class MainActivity : AppCompatActivity() {
scanner?.start()
}

/** Ask the host to open its bundled GoConsoleOS USB Installer tool. */
private fun openUsbInstaller(host: ConsoleHost) {
var client: LinkClient? = null
client = LinkClient(host, object : LinkClient.Listener {
override fun onConnected(h: ConsoleHost) {
client?.sendControl(Protocol.MSG_PAIR, JSONObject().put("action", "open-usb-installer"))
client?.close()
}
override fun onControl(type: String, payload: JSONObject) {}
override fun onFrame(type: Int, payload: ByteArray) {}
override fun onDisconnected(error: String?) {}
})
client.connect()
}

override fun onDestroy() {
DeviceServer.stop()
scanner?.stop()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package com.gostudios.console.portable

import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.gostudios.console.sdk.ConsoleHost
import com.gostudios.console.sdk.LinkClient
import com.gostudios.console.sdk.Protocol
import org.json.JSONArray
import org.json.JSONObject

/**
* GoConsoleOS Portable — full tool panel for the remote host.
*
* Connects to the GoConsoleOS console on the LAN, lists the host tools it can
* drive (GoUsbMaker / USB Health / Cast / GoAI / Store / Screenshot / ...) and
* triggers the selected one by sending a control message back to the host.
*/
class PortableActivity : AppCompatActivity() {

private var client: LinkClient? = null
private lateinit var adapter: ToolAdapter
private val tools = mutableListOf<ToolRow>()

/** Default tool catalogue shown when the host does not support `tools.list`. */
private val defaults = listOf(
ToolRow("usb-installer", "GoUsbMaker", "Build a Portable USB Gaming Console"),
ToolRow("usb-health", "USB Health", "SMART report for every USB console"),
ToolRow("cast", "GoConsoleOS Cast", "Mirror your console to a TV or device"),
ToolRow("goai", "GoAI", "Ask your assistant anything, locally"),
ToolRow("store", "GoStore", "Browse the curated app catalogue"),
ToolRow("screenshot", "Screenshot", "Capture the current screen on the host"),
)

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_portable)

val hostJson = intent.getStringExtra(EXTRA_HOST) ?: return
val host = ConsoleHost.fromJson(JSONObject(hostJson))

val rv = findViewById<RecyclerView>(R.id.toolList)
rv.layoutManager = LinearLayoutManager(this)
adapter = ToolAdapter(tools) { tool -> runTool(tool) }
rv.adapter = adapter

client = LinkClient(host, object : LinkClient.Listener {
override fun onConnected(h: ConsoleHost) {
runOnUiThread {
findViewById<TextView>(R.id.txtConn).text =
getString(R.string.status_connected, h.name)
}
client?.sendControl(MSG_TOOLS_LIST)
}

override fun onControl(type: String, payload: JSONObject) {
if (type == MSG_TOOLS_LIST) {
applyTools(payload.optJSONArray("tools"))
} else if (type == MSG_TOOLS_RUN) {
runOnUiThread {
findViewById<TextView>(R.id.txtConn).text =
getString(R.string.tool_ok, payload.optString("tool", ""))
}
}
}

override fun onFrame(type: Int, payload: ByteArray) {}
override fun onDisconnected(error: String?) {
runOnUiThread {
findViewById<TextView>(R.id.txtConn).text = "Disconnected: $error"
}
}
})
client?.connect()
}

override fun onDestroy() {
client?.close()
super.onDestroy()
}

private fun runTool(tool: ToolRow) {
val c = client ?: return
if (tool.id == "usb-installer") {
c.sendControl(Protocol.MSG_PAIR, JSONObject().put("action", "open-usb-installer"))
} else {
c.sendControl(MSG_TOOLS_RUN, JSONObject().put("tool", tool.id))
}
findViewById<TextView>(R.id.txtConn).text =
getString(R.string.tool_running, tool.name)
}

private fun applyTools(arr: JSONArray?) {
val rows: List<ToolRow> = if (arr == null || arr.length() == 0) {
defaults
} else {
buildList {
for (i in 0 until arr.length()) {
val item = arr.optJSONObject(i) ?: continue
add(
ToolRow(
item.optString("id", ""),
item.optString("name", ""),
item.optString("desc", ""),
),
)
}
}.ifEmpty { defaults }
}
runOnUiThread {
tools.clear()
tools.addAll(rows)
adapter.notifyDataSetChanged()
}
}

private class ToolRow(val id: String, val name: String, val desc: String)

private class ToolAdapter(
private val items: MutableList<ToolRow>,
private val onClick: (ToolRow) -> Unit,
) : RecyclerView.Adapter<ToolAdapter.Holder>() {

class Holder(view: View) : RecyclerView.ViewHolder(view) {
val name: TextView = view.findViewById<TextView>(R.id.toolName)
val desc: TextView = view.findViewById<TextView>(R.id.toolDesc)
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
val v = LayoutInflater.from(parent.context)
.inflate(R.layout.item_tool, parent, false)
return Holder(v)
}

override fun onBindViewHolder(holder: Holder, position: Int) {
val tool = items[position]
holder.name.text = tool.name
holder.desc.text = tool.desc
holder.itemView.setOnClickListener { onClick(tool) }
}

override fun getItemCount() = items.size
}

companion object {
private const val EXTRA_HOST = "hostJson"
private const val MSG_TOOLS_LIST = "tools.list"
private const val MSG_TOOLS_RUN = "tools.run"

fun intent(context: Context, host: ConsoleHost): Intent =
Intent(context, PortableActivity::class.java)
.putExtra(EXTRA_HOST, host.toJson().toString())
}
}
36 changes: 36 additions & 0 deletions apps/portable/src/main/res/layout/activity_portable.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#0A0F1E">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/portable_title"
android:textColor="#00C9DB"
android:textSize="26sp"
android:textStyle="bold"
android:padding="16dp"
android:paddingBottom="0dp" />

<TextView
android:id="@+id/txtConn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/portable_subtitle"
android:textColor="#7A80A0"
android:textSize="13sp"
android:padding="12dp"
android:paddingTop="0dp" />

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/toolList"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:clipToPadding="false"
android:padding="8dp" />

</LinearLayout>
26 changes: 26 additions & 0 deletions apps/portable/src/main/res/layout/item_tool.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="14dp"
android:layout_margin="6dp"
android:background="@drawable/bg_card">

<TextView
android:id="@+id/toolName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#E8E8F5"
android:textSize="17sp"
android:textStyle="bold" />

<TextView
android:id="@+id/toolDesc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#8A90B0"
android:textSize="13sp"
android:layout_marginTop="2dp" />

</LinearLayout>
6 changes: 5 additions & 1 deletion apps/portable/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
<string name="btn_link">GoConsoleOS Link</string>
<string name="btn_usbhealth">USB Health</string>
<string name="btn_cast">Cast</string>
<string name="btn_boot">Open USB Installer</string>
<string name="btn_boot">Portable App</string>
<string name="tool_running">Starting %1$s…</string>
<string name="tool_ok">%1$s sent to the console.</string>
<string name="portable_title">GoConsoleOS Portable App</string>
<string name="portable_subtitle">Tools for your GoConsoleOS on USB</string>
<string name="progress">%1$d%%</string>
</resources>
12 changes: 12 additions & 0 deletions docs/PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,20 @@ The stream mixes two kinds of records:
{"type":"cast.start"}\n
{"type":"cast.stop"}\n
{"type":"pair","action":"open-usb-installer"}\n
{"type":"tools.list"}\n
{"type":"tools.run","tool":"goai"}\n
```

`tools.list` replies with the Portable App tool catalogue the host supports:

```json
{"type":"tools.list","tools":[{"id":"usb-installer","name":"GoUsbMaker","desc":"..."},{"id":"goai","name":"GoAI","desc":"..."}]}
```

`tools.run` triggers that tool on the host (`usb-installer`, `usb-health`,
`cast`, `goai`, `store`, `screenshot`) and replies
`{"type":"tools.run","ok":true,"tool":"..."}`.

### 2. Binary frames

5-byte header: `1 byte type` + `4 byte big-endian length`, then payload.
Expand Down
6 changes: 6 additions & 0 deletions shared/src/main/java/com/gostudios/console/sdk/Protocol.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ object Protocol {
const val MSG_CAST_START = "cast.start"
const val MSG_CAST_STOP = "cast.stop"

/** List remote host tools (usb-installer, usb-health, cast, goai, ...). */
const val MSG_TOOLS_LIST = "tools.list"

/** Run a remote host tool by id. */
const val MSG_TOOLS_RUN = "tools.run"

const val USB_HEALTH_OK = "ok"
const val USB_HEALTH_FAIR = "fair"
const val USB_HEALTH_POOR = "poor"
Expand Down
Loading