Skip to content

Commit 330df39

Browse files
authored
feat: add support to push flightplans directly to device via WebUSB ADB (#570)
* feat: add support to push flightplans directly to device webadb * refactor: clarify the flightplan is sent to 'controller'
1 parent 331562f commit 330df39

File tree

4 files changed

+248
-4
lines changed

4 files changed

+248
-4
lines changed

src/frontend/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
"@turf/length": "^7.2.0",
2525
"@turf/meta": "^7.2.0",
2626
"@turf/transform-rotate": "^7.2.0",
27+
"@yume-chan/adb": "^2.1.0",
28+
"@yume-chan/adb-credential-web": "^2.1.0",
29+
"@yume-chan/adb-daemon-webusb": "^2.1.0",
2730
"autoprefixer": "^10.4.20",
2831
"axios": "^1.7.9",
2932
"class-variance-authority": "^0.6.1",
@@ -92,7 +95,7 @@
9295
},
9396
"name": "drone-tm",
9497
"private": true,
95-
"license": "(AGPL-3.0-only OR CC-BY-4.0)",
98+
"license": "(AGPL-3.0-only OR CC-BY-4.0)",
9699
"scripts": {
97100
"build": "tsc && vite build",
98101
"dev": "vite",

src/frontend/pnpm-lock.yaml

Lines changed: 76 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/frontend/src/components/DroneOperatorTask/DescriptionSection/index.tsx

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import useWindowDimensions from '@Hooks/useWindowDimensions';
99
import hasErrorBoundary from '@Utils/hasErrorBoundary';
1010
import MapSection from '../MapSection/MapSection';
1111
import DescriptionBox from './DescriptionBox';
12+
import { sendDjiGoFileViaAdb, sendPotensicProFileViaAdb } from '@Utils/adb';
1213

1314
const { BASE_URL } = process.env;
1415

@@ -60,11 +61,38 @@ const DroneOperatorDescriptionBox = () => {
6061
window.URL.revokeObjectURL(url);
6162
})
6263
.catch(error =>
63-
toast.error(`There wan an error while downloading file
64+
toast.error(`There was an error while downloading file
6465
${error}`),
6566
);
6667
};
6768

69+
const sendFlightPlanViaAdb = async () => {
70+
try {
71+
const response = await fetch(
72+
`${BASE_URL}/waypoint/task/${taskId}/?project_id=${projectId}&download=true&mode=${waypointMode}&drone_type=${droneModel}&rotation_angle=${rotationAngle}`,
73+
{ method: 'POST' },
74+
);
75+
76+
if (!response.ok) {
77+
throw new Error(`Network response was ${response.statusText}`);
78+
}
79+
80+
const blob = await response.blob();
81+
82+
// TODO improve this logic to be more generic
83+
if (droneModel === 'POTENSIC_ATOM_2') {
84+
await sendPotensicProFileViaAdb(blob);
85+
} else {
86+
await sendDjiGoFileViaAdb(blob);
87+
}
88+
89+
toast.success(`Flight plan sent to device!`);
90+
} catch (error) {
91+
console.error(error);
92+
toast.error(`There was an error while sending file: ${error}`);
93+
}
94+
};
95+
6896
const downloadFlightPlanGeojson = () => {
6997
if (!rotatedFlightPlanData?.geojsonListOfPoint) return;
7098

@@ -104,7 +132,7 @@ const DroneOperatorDescriptionBox = () => {
104132
window.URL.revokeObjectURL(url);
105133
})
106134
.catch(error =>
107-
toast.error(`There wan an error while downloading file
135+
toast.error(`There was an error while downloading file
108136
${error}`),
109137
);
110138
};
@@ -131,7 +159,7 @@ const DroneOperatorDescriptionBox = () => {
131159
window.URL.revokeObjectURL(url);
132160
})
133161
.catch(error =>
134-
toast.error(`There wan an error while downloading file
162+
toast.error(`There was an error while downloading file
135163
${error}`),
136164
);
137165
};
@@ -156,6 +184,18 @@ const DroneOperatorDescriptionBox = () => {
156184
</Button>
157185
{showDownloadOptions && (
158186
<div className="naxatw-absolute naxatw-right-0 naxatw-top-10 naxatw-z-20 naxatw-w-[200px] naxatw-rounded-sm naxatw-border naxatw-bg-white naxatw-shadow-2xl">
187+
<div
188+
className="naxatw-cursor-pointer naxatw-px-3 naxatw-py-2 hover:naxatw-bg-redlight"
189+
role="button"
190+
tabIndex={0}
191+
onKeyDown={() => sendFlightPlanViaAdb()}
192+
onClick={() => {
193+
sendFlightPlanViaAdb();
194+
setShowDownloadOptions(false);
195+
}}
196+
>
197+
Send flight plan file to controller
198+
</div>
159199
<div
160200
className="naxatw-cursor-pointer naxatw-px-3 naxatw-py-2 hover:naxatw-bg-redlight"
161201
role="button"

src/frontend/src/utils/adb.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { Adb, AdbDaemonTransport, AdbShellProtocolProcess, encodeUtf8 } from "@yume-chan/adb";
2+
import AdbWebCredentialStore from "@yume-chan/adb-credential-web";
3+
import { AdbDaemonWebUsbDevice, AdbDaemonWebUsbDeviceManager } from "@yume-chan/adb-daemon-webusb";
4+
5+
6+
async function sendDjiGoFileViaAdb(data: Blob) {
7+
const adb = await _getAdbConnection();
8+
if (!adb) return;
9+
10+
const base64String = await _encodeDataAsBase64String(data);
11+
12+
// Need to find first existing waypoint directory available
13+
const waypointDir = `/sdcard/Android/data`;
14+
const listDirs = await adb.subprocess.shellProtocol!.spawn(
15+
`ls -1 ${waypointDir}`
16+
);
17+
const dirOutput = await _readShellOutput(listDirs);
18+
const dirs = dirOutput.split('\n').filter(Boolean);
19+
if (dirs.length === 0) {
20+
throw new Error(`A waypoint flight must flown first`);
21+
}
22+
const firstDir = dirs[0];
23+
// Then find the .kmz file within that dir
24+
const targetPath = `${waypointDir}/${firstDir}`;
25+
const listFiles = await adb.subprocess.shellProtocol!.spawn(
26+
`ls -1 ${targetPath}/*.kmz`
27+
);
28+
const filesOutput = await _readShellOutput(listFiles);
29+
const kmzFiles = filesOutput.split('\n').filter(Boolean);
30+
if (kmzFiles.length === 0) {
31+
throw new Error(`No .kmz files found in ${targetPath}`);
32+
}
33+
const targetFile = kmzFiles[0];
34+
console.log(`Replacing: ${targetFile}`);
35+
36+
// // Send file to phone via ADB STDIN
37+
const process = await adb.subprocess.shellProtocol!.spawn(
38+
`sh -c "base64 -d > '${targetFile}'"`
39+
);
40+
const writer = process.stdin.getWriter();
41+
await writer.write(encodeUtf8(base64String));
42+
console.log(`Successfully replaced: ${targetFile}`);
43+
console.log(`Copied flightplan to ${targetFile}`)
44+
}
45+
46+
async function sendPotensicProFileViaAdb(data: Blob) {
47+
const adb = await _getAdbConnection();
48+
if (!adb) return;
49+
50+
const base64String = await _encodeDataAsBase64String(data);
51+
52+
// Cleanup old journal files
53+
await adb.subprocess.shellProtocol!.spawn("run-as com.ipotensic.potensicpro rm -f databases/map.db-journal");
54+
console.log('Deleted db journal')
55+
56+
// Send file to phone via ADB STDIN
57+
const process = await adb.subprocess.shellProtocol!.spawn(
58+
`run-as com.ipotensic.potensicpro sh -c "base64 -d > databases/test.db"`
59+
);
60+
const writer = process.stdin.getWriter();
61+
// Send as UTF-8 encoded data
62+
await writer.write(encodeUtf8(base64String));
63+
console.log('Copied flightplan to databases/map.db')
64+
}
65+
66+
async function _readShellOutput(process: AdbShellProtocolProcess): Promise<string> {
67+
const decoder = new TextDecoder();
68+
const reader = process.stdout.getReader();
69+
let output = "";
70+
while (true) {
71+
const { value, done } = await reader.read();
72+
if (done) break;
73+
output += decoder.decode(value);
74+
}
75+
return output;
76+
}
77+
78+
async function _encodeDataAsBase64String(data: Blob): Promise<string> {
79+
return await new Promise<string>((resolve) => {
80+
const reader = new FileReader();
81+
reader.onload = () => {
82+
const arrayBuffer = reader.result as ArrayBuffer;
83+
const bytes = new Uint8Array(arrayBuffer);
84+
let binary = "";
85+
for (let i = 0; i < bytes.byteLength; i++) {
86+
binary += String.fromCharCode(bytes[i]);
87+
}
88+
resolve(btoa(binary));
89+
};
90+
reader.readAsArrayBuffer(data);
91+
});
92+
}
93+
94+
async function _getAdbConnection(): Promise<Adb | undefined> {
95+
const Manager: AdbDaemonWebUsbDeviceManager | undefined = AdbDaemonWebUsbDeviceManager.BROWSER;
96+
97+
if (!Manager) {
98+
alert("WebUSB is not supported in this browser");
99+
return;
100+
}
101+
102+
const CredentialStore = new AdbWebCredentialStore();
103+
104+
const device: AdbDaemonWebUsbDevice | undefined = await Manager.requestDevice();
105+
if (!device) {
106+
alert("No device selected");
107+
return;
108+
}
109+
110+
const connection = await device.connect();
111+
const adb = new Adb(
112+
await AdbDaemonTransport.authenticate({
113+
serial: device.serial,
114+
connection,
115+
credentialStore: CredentialStore,
116+
})
117+
);
118+
119+
return adb;
120+
}
121+
122+
export {
123+
sendDjiGoFileViaAdb,
124+
sendPotensicProFileViaAdb,
125+
}

0 commit comments

Comments
 (0)