// TODO: write this post
// TODO: write this post
This is an attempt at reverse-engineering the DJI Ronin SC camera gimbal with the following goals:
This blog post serves both as my personal notepad and as a starting point for anyone else going down the same path in the future (perhaps with a different DJI device). As the work is still in progress, the post will be updated occasionally.
Since this is a rather expensive gadget, I’d like to avoid taking it apart as much as possible. Thankfully, the FCC publishes independent test reports of all RF-emitting gadgets sold in the US, including photos of the wiring and PCBs: https://fccid.io/2ANDR-R181902. Since many of the same accessories fit the Ronin S as well, its FCC report is also quite useful: https://fccid.io/2ANDR-RS11804. If you see photos of components on a blue mat in this article, I’ve likely clipped them from these.
The gimbal is split into two main parts – the battery grip and the gimbal head+control unit. Connecting the two is a set of 6 flat contacts on the grip with corresponding pogo pins on the head unit. Presumably, these carry only power, but I have not bothered confirming this.
The gimbal has two sets of side accessory connectors, this time with 8 flat pads on the head and corresponding pogo pins on the accessories. Detaching them from the body by removing the 4 hex screws holding them in place reveals a conveniently labeled PCB and rather annoyingly tiny internal header.
A few minutes of poking with the smallest multimeter probe I could find reveals the following correspondence:
//TODO: include pinout
TL;DR: adjustable by default. If you want to lock it to a specific voltage, cut the line above ADJ and solder across the pads next to the voltage you want.
Let’s say you have two servers: Cabbage and BEETroot (naming your servers after vegetables is normal, right?). Cabbage hosts a database and some other services and BEETroot hosts your application. You want them to be able to connect to each other securely, without having to deal with securing every one of the services to go over the Internet.
This guide was written for Ubuntu Server 18.04, but should be valid for any Linux system (aside from the installation steps).
sudo add-apt-repository ppa:wireguard/wireguard
sudo apt-get install wireguard
On each server, a private key needs to be generated and a public key derived from it. For convenience, the bottom command will save both to files, named after the server’s hostname:
user@both-servers:~ $ wg genkey | tee "$(hostname -s).wgpriv" | wg pubkey > "$(hostname -s).wgpub"
Both should look similar to this: gMFBuyVWbx/O9DAn2ajAhnNN0GAfZTM8u7d0HTJoqWs=
The Wireguard config files reside in /etc/wireguard/. They are ini files, use the .conf extension and are named after their Wireguard interfaces (wg[number]).
You will also need to decide on a port (which you will have to open) and private IP address for each of the servers (on the same subnet, of course).
Our convention is to use the same iface name and port (starting at 51820) on both sides of a P-P link and IPv4 addresses in a /31 subnet. (iface wg[n] ipv4 10.0.[n].[0,1]/31 port 51820+[n]
)
Edit /etc/wireguard/wg[n].conf with root privs:
# P-P link to [other side]
# Our config
[Interface]
Address = [our INTERNAL address]/31
PrivateKey = [our privkey]
ListenPort = [our port]
# [other side]
[Peer]
PublicKey = [other side's pubkey]
AllowedIPs = [other side's INTERNAL address]/31
Endpoint = [other side's EXTERNAL address]:[other side's port]
An example for Cabbage would therefore be:
# P-P link to BEETroot
# Our config
[Interface]
Address = 10.0.0.1/31
PrivateKey = gMFBuyVWbx/O9DAn2ajAhnNN0GAfZTM8u7d0HTJoqWs=
ListenPort = 51820
# BEETroot
[Peer]
PublicKey = omFWU/kRAAQOU+31j6RoIPA7HLVSqW67BvQuZ9z1uxA=
AllowedIPs = 10.0.0.0/31
Endpoint = beetroot.example.com:51820
Assuming the previous example (wg0):
# Apply config
user@both-servers:~ $ sudo wg-quick up wg0
# Start the connection
user@both-servers:~ $ systemctl start wg-quick@wg0
# Make start on startup (optional)
user@both-servers:~ $ systemctl enable wg-quick@wg0
Let’s see if it works (example output):
user@Cabbage:~ $ sudo wg show
interface: wg0
public key: omFWU/kRAAQOU+31j6RoIPA7HLVSqW67BvQuZ9z1uxA=
private key: (hidden)
listening port: 51820
peer: omFWU/kRAAQOU+31j6RoIPA7HLVSqW67BvQuZ9z1uxA=
endpoint: 198.51.100.42:51820
allowed ips: 10.1.0.0/31
latest handshake: 1 second ago
transfer: 148 B received, 92 B sent
user@Cabbage:~ $ ping 10.1.0.0 -c 1
PING 10.1.0.0 (10.1.0.0) 56(84) bytes of data.
64 bytes from 10.1.0.0: icmp_seq=1 ttl=64 time=46.1 ms
--- 10.1.0.0 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 46.174/46.174/46.174/0.000 ms
(WORK IN PROGRESS)
OPi pin | Purpose | LCD pin |
1 | 3.3 V | 1 |
2 or 4 | 5 V | 4 |
6 or [see chart] | GND | 6 |
19 | SPI | 19 |
21 | 21 | |
/dts-v1/;
/plugin/;
/ {
compatible = "allwinner,sun8i-h3";
fragment@0 {
target = <&spi0>;
__overlay__ {
status = "okay";
spidev@0{
status = "disabled";
};
spidev@1{
status = "disabled";
};
};
};
fragment@1 {
target = <&pio>;
__overlay__ {
// Set up pins
ili9486_pins: ili9486_pins {
allwinner,pins = "PA2", "PC7"; // Add DC/RS nad RST pins here
allwinner,function = "gpio_in";
};
};
};
fragment@2 {
target = <&spi0>;
__overlay__ {
/* needed to avoid dtc warning */
#address-cells = <1>;
#size-cells = <0>;
ili9486: ili9486@0{
compatible = "ilitek,ili9486";
reg = <0>;
pinctrl-names = "default";
pinctrl-0 = <&ili9486_pins>;
spi-max-frequency = <16000000>;
txbuflen = <32768>;
rotate = <90>;
bgr = <0>;
fps = <30>;
buswidth = <8>;
regwidth = <16>;
// Select pins: 1st number = pos. of letter in the alphabet - 1
// 2nd number = pin number
// 3rd number = DON'T CHANGE! (1 = ACTIVE_LOW, 0 = ACTIVE_HIGH)
// Example: PA2 = <&pio 0 2 1>
// PC7 = <&pio 2 7 0>
reset-gpios = <&pio 0 2 1>; // RST pin
dc-gpios = <&pio 2 7 0>; // DC/RS pin
debug = <0>;
init = <0x10000b0 0x00
0x1000011
0x20000ff
0x100003a 0x55
0x1000036 0x28
0x10000c2 0x44
0x10000c5 0x00 0x00 0x00 0x00
0x10000e0 0x0f 0x1f 0x1c 0x0c 0x0f 0x08 0x48 0x98 0x37 0x0a 0x13 0x04 0x11 0x0d 0x00
0x10000e1 0x0f 0x32 0x2e 0x0b 0x0d 0x05 0x47 0x75 0x37 0x06 0x10 0x03 0x24 0x20 0x00
0x10000e2 0x0f 0x32 0x2e 0x0b 0x0d 0x05 0x47 0x75 0x37 0x06 0x10 0x03 0x24 0x20 0x00
0x1000036 0x28
0x1000011
0x1000029>;
};
};
};
};
Display module pinout – “Official” product wiki – http://www.lcdwiki.com/MHS-3.5inch_RPi_Display#Hardware_Description
DTS template – Armbian forums – https://forum.armbian.com/topic/11701-35-lcd-ili9486-with-orange-pi-zero/
Misc. information about a similar module – Armbian forums – https://forum.armbian.com/topic/4656-orangepi-zero-mainline-kernel-spi-lcd-touchscreen/
We think of SSH as just a simple Secure SHell, but did you know it can also forward ports? Yes, of course you did! We’ve all probably read this exact sentence in probably hundreds if not thousands of articles out there. What you probably haven’t read, is an article that summarizes all of the different options in a nice, concise and copy-pastable way.
(TODO)
You’re in a closed network – NAT, firewall, trigger-happy intrusion detection, etc. You have a service running at a local address on a local port. You have a remote server with SSH. You want to access remote server on remote port and have your traffic be tunneled to your internal service.
user@[local machine]:~/ $ ssh [remote server] -R [remote port]:[local address]:[local port]
Example:
I’m at work on my workstation. I’m running a crappy NodeJS app on localhost port 80. I want to show it off to a recruiter because I hate this job, but the sysadmin knows I want to leave so he won’t let me forward a port. I have a cheap VPS at cheap-vps.no-ip.org and want expose my app on port 8080.
user@workstation:~/ $ ssh shitty-vps.no-ip.org -R 8080:localhost:80
My shitty web app will now be accessible at http://shitty-vps.no-ip.org:8080/ and I will soon be safe from the stale coffee and bad hours of my current employer.
This whole project started several months ago, when I discovered the most amazing thing: 5m of RGB LEDs can be had for less than 5€ on AliExpress!
The idea was simple: my phone has a notification LED that blinks with different colors to signal that there are unread notifications, but since my phone is usually on my desk in a flip case (and probably under a stack of paper), I don’t usually see it. So how neat would it be, if my entire room mirrored that LED so I never missed a notification again?
If you don’t care about the LEDs and control circuits themselves and just want to know how I achieved notification LED mirroring, skip to this section.
The RGB(W) strip I chose is the RGBWW 5050 model. The WW part stands for Warm White, meaning the actual RGB LEDs alternate with warm white LEDs and the 5050 refers to the RGB LED model, which is the stronger of the two most common ones (the other being 3528).
I opted for RGBW instead of standard RBG as I wanted to be able to flash a notification color while still illuminating my room enough to work. The warm white (as opposed to a more standard, cooler white) is just a personal preference.
I was initially planning on using my home NAS server to control the LEDs with just a long USB extension cable running to them, but looking at the pile of ESP8266s on my desk gave me a better idea: use esp-link to expose the driver board’s serial port over the network and control it directly.
As the ESP8266 requires a 3,3V supply, I had to step down some of the existing 5V even further. Luckily, I had some step-down boards handy and just jerry-rigged them onto the main board with some wires.
I loaded the ESP8266 with esp-link, which involved first updating its bootloader and then flashing the esp-link binary onto the chip. The configuration was also rather simple and involved connecting to an open Wi-Fi hotspot, created by the chip and entering the credentials to my home Wi-Fi, after which the ESP switched to station mode and acted just like any other device on my network.
I proceeded to connect the TX, RX and RST pins of my MCU to the ESP according to the official instructions, which gave me access to its serial port from any device on the network, as well as allowed me to remotely flash the MCU’s firmware at any time.
The original firmware consisted of some 200 lines of C that were about as fast and fexible as an 80-year-old with osteoporosis. You can see it [here], but I don’t advise you to.
The firmware that I use now is actually not terrible and is available on GitLab [here]. You’re free to use it if you want to – it’s all GPLv2.
Because I can’t be bothered with proper documentation, here’s a short snippet that shows off the functionality (> is the prompt)
> asdf
E Invalid command!
> P ("print" the current state of the LEDs)
PR255G0B0W150
> SB150W0 ("set" specified channels to new values)
PR255G0B150W0
> FR0G0B0W0 ("fade" specified channels to new values
PR0G0B0
The firmware allows you to define (at compile time) any number of channels, each with a channel name (a single character) and its corresponding PWM output pin. You can see some examples in the README.
Ok, now for the interesting part. The never-before-seen part. The only part that hasn’t been posted all over Instructables 100s of times. How can I monitor the state of my phone’s notification LED and send that information to the light server?
Not all that surprisingly, there isn’t a simple function in the Android SDK that would allow me to just do that. So, I had to get creative and dig deeper into the system itself.
Looking through the sources of the particular ROM I was using (based on LineageOS, although the same applies for stock Android, too), [………..]
Once I know what function I am interested in, I need a way to tap into it and mess with its execution. Fortunately, that is exactly what the XPosed Framework was designed to do. After taking care of all the boilerplate required for XPosed to load my module into the right binary (which can be found here), I was able to tap the […] function like so:
public class Main implements IXposedHookLoadPackage {
private static final String NMS_CLASS = "com.android.server.notification.NotificationManagerService";
@Override public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
if (lpparam.packageName.equals("android") && lpparam.processName.equals("android")) {
XposedBridge.log("Hooked android");
XposedHelpers.findAndHookMethod(NMS_CLASS, lpparam.classLoader, "updateLightsLocked", methodHook);
}
}
}
This allows me to execute my code every time that function is called, just before it starts executing, giving me full control over the parameters it receives. Using a possibly one of the ugliest parts of Java – reflection, I was finally, after hours of staring at Android source code and dumping strange variables to logcat (because setting up an OS-level debugger is simply too much hassle), able to understand what all the variables meant and how they related to the LED’s action and came up with this beautiful mess of spaghetti code:
static XC_MethodHook methodHook = new XC_MethodHook() {
@Override
protected void afterHookedMethod(final MethodHookParam param) throws Throwable {
// type: NotificationManagerService
Object nms = param.thisObject;
Field mNotificationLightField = NotificationManagerService.getDeclaredField("mNotificationLight");
mNotificationLightField.setAccessible(true);
// type: Light
Object mNotificationLight = mNotificationLightField.get(nms);
Class Light = mNotificationLight.getClass()
Field mColorField = Light.getDeclaredField("mColor");
mColorField.setAccessible(true);
int mColor = mColorField.getInt(mNotificationLight);
Field mFlashingField = Light.getDeclaredField("mFlashing");
mFlashingField.setAccessible(true);
boolean mFlashing = mFlashingField.getBoolean(mNotificationLight);
XposedBridge.log("Light color: " + mColor);
XposedBridge.log("Light flashing: " + mFlashing);
}
};
With all the pieces working, it was time to connect everything together. For every light event, after the above code decoded it, the details of the event (ON/OFF, color, blinking speed) would be send to the light server in the form of a special command. The server would forward it to the MCU, where the firmware would read the details and save them into its memory.
With the LED state information now synchronized to the controller, it was simply a matter of writing a fade function to mimic the one used by my phone and the project was done.
We’ve all been there. You’re all set for your big presentation: beautiful template, just the right amount of text, fancy graphics and that one perfectly timed star wipe for comedic effect. You’re just about to leave when you remember, that the world isn’t an Apple commercial. You pack the HDMI-VGA adapter and a second one for backup, your brand new absurdly expensive slide clicker, you make sure your presentations looks good on a 4:3 screen too and walk into the hall with the confidence of a Python developer, only to be greeted by a hard-wired Windows XP computer with Office 2007…
I won’t bother you with any more stories. Here’s what I want: I want to be able to walk around with a tablet in my hand, the presenter notes on it and generally run whatever I want on it without having to worry about compatibility ever again. Why? Mostly because it looks futuristic and cool.
Let’s get some of the obvious solutions out of the way first:
If you want to use this setup, here’s what you’ll need:
The trickiest part of this setup is the virtual display. I don’t want to stream my laptop’s display, because I want to have my presenter view on it. I want Windows to think there’s an actual extra physical monitor connected to it. This is something that is very hard to achieve, but luckily, some Windows driver magicians over at datronicsoft have already done that and packaged into a rather neat application called spacedesk.
After installing the spacedesk driver on the laptop, I can connect to it from any device on the network using one of several available viewers: Windows, Android, iOS and HTML5. I’m interested in the HTML5 one because it is as cross-platform as anything can possibly be.
Spacedesk’s HTML5 viewer is normally available on their website, but for several reasons, I want to serve the files to the target computer myself. A quick CTRL+S gets me the page and all of its JS into a folder that I can serve with a simple web server on my phone.
Out-of-the-box, the viewer doesn’t support Mozilla Firefox for “performance reasons”, but in my testing, it works just fine and I still prefer a slow connection to no connection at all, so a simple search-and-replace on the source code gets rid of the browser checks:
sed -i 's/browser.name === "Firefox"/\0 \&\& false/' spacedesk.min.js
Some further modification was done to pre-populate the address field with the (static) address of the phone’s tethering interface.
A simple httpd from Busybox will suffice to serve the files from the phone and present them to the target computer.
// TODO
// TODO
The root filesystem is rootfs
, which is stored in memory and therefore wiped on reboot. The Micro SD card is mounted at /tmp/fuse_d/
. Something is also mounted at /tmp/fuse_a
and /tmp/fuse_z
.
1234567890
192.168.42.1
Opened ports on 192.168.42.1
Discovered open port 80/tcp on 192.168.42.1
Discovered open port 554/tcp on 192.168.42.1
Discovered open port 53/tcp on 192.168.42.1
Discovered open port 8787/tcp on 192.168.42.1
Discovered open port 7878/tcp on 192.168.42.1
To enable shell access you must create an empty file on the SD card named enable_info_display.script
. Then you can connect to the camera using telnet:
telnet 192.168.42.1 23
You will be presented a login promt:
buildroot login:
Type root
and hit enter. You are now connected to the camera’s root Linux shell
Technically just a raw TCP socket connection, but it’s easier to just call it telnet.
telnet 192.168.42.1 7878
Error messages sent by the camera:
{"rval": -7}
– Input is not a valid JSON object{"rval":-4,"msg_id":0}
– Input object is empty{"rval":-9,"msg_id":0}
– Input object is not a valid commandAll requests require a token you have to request when connecting to the camera:
Request:
{"msg_id":257, "token":0}
Response:
{ "rval": 0, "msg_id": 257, "param": 1 }
param
is your token. All requests in this article have the token set to 1. This will be different for you.
Request:
{"msg_id":3, "token":1}
Response:
NOTE: The config is a list of objects of settings, not just an object of settings. You have to use a loop!
{ "rval": 0, "msg_id": 3, "param": [ {"camera_clock": "2015-04-07 02:32:29"}, {"video_standard": "NTSC"}, {"app_status": "idle"}, {"video_resolution": "1920x1080 60P 16:9"}, {"video_stamp": "off"}, {"video_quality": "S.Fine"}, {"timelapse_video": "off"}, {"capture_mode": "precise quality"}, {"photo_size": "16M (4608x3456 4:3)"}, {"photo_stamp": "off"}, {"photo_quality": "S.Fine"}, {"timelapse_photo": "60"}, {"preview_status": "on"}, {"buzzer_volume": "mute"}, {"buzzer_ring": "off"}, {"capture_default_mode": "precise quality"}, {"precise_cont_time": "60.0 sec"}, {"burst_capture_number": "7 p / s"}, {"restore_factory_settings": "on"}, {"led_mode": "all enable"}, {"dev_reboot": "on"}, {"meter_mode": "center"}, {"sd_card_status": "insert"}, {"video_output_dev_type": "tv"}, {"sw_version": "YDXJv22_1.0.7_build-20150330113749_b690_i446_s699"}, {"hw_version": "YDXJ_v22"}, {"dual_stream_status": "on"}, {"streaming_status": "off"}, {"precise_cont_capturing": "off"}, {"piv_enable": "off"}, {"auto_low_light": "on"}, {"loop_record": "off"}, {"warp_enable": "off"}, {"support_auto_low_light": "on"}, {"precise_selftime": "5s"}, {"precise_self_running": "off"}, {"auto_power_off": "5 minutes"}, {"serial_number": "xxxxx"}, {"system_mode": "capture"}, {"system_default_mode": "capture"}, {"start_wifi_while_booted": "off"}, {"quick_record_time": "0"}, {"precise_self_remain_time": "0"}, {"sdcard_need_format": "no-need"}, {"video_rotate": "off"} ] }
Request:
{"msg_id":769,"token":1}
Response:
{ "msg_id": 7, "type": "start_photo_capture", "param":"precise quality;off"}
{ "msg_id": 7, "type": "photo_taken", "param":"/tmp/fuse_d/DCIM/100MEDIA/YDXJ0047.jpg"}
param
is the full path of the imageNOTE: To get the download URL of the image, replace /tmp/fuse_d/
with http://192.168.42.1/
.
All messages with msg_id: 7
are events and are sent automatically by the camera.
{ "msg_id": 7, "type": "<string: event name>"}
Some events also provide additional data in the param
attribute:
{ "msg_id": 7, "type": "<string: event name>", "param":"<int: event data>"}
Below is a list of known events, their parameters and an example for each
Fired when the battery level changes
{ "msg_id": 7, "type": "battery", "param":"20"}
type
is battery
when discharging and adapter
when chargingparam
is the battery charge percentage (int 1-100, obviously)Fired when the USB charging cable is connected/disconnected
{ "msg_id": 7, "type": "adapter_status" ,"param":"1"}
param
:0
– cable disconnectedparam
:1
– cable connectedFired when the recording mode is changed (big button on the front)
{ "msg_id": 7, "type": "switch_to_rec_mode" }
type
:"switch_to_rec_mode"
– switched to video modetype
:"switch_to_cap_mode"
– switched to photo mode{ "msg_id": 7, "type": "start_photo_capture" ,"param":"precise quality;off"}
{ "msg_id": 7, "type": "precise_capture_data_ready" }
{ "msg_id": 7, "type": "photo_taken" ,"param":"/tmp/fuse_d/DCIM/100MEDIA/YDXJ0513.jpg"}
Fired when the SD card is inserted/removed
{ "msg_id": 7, "type": "sd_card_status" ,"param":"insert"}
type
:"insert"
– SD card insertedtype
:"remove"
– SD card removed
When SD card is removed, the following event also fires:{ "msg_id": 7, "type": "CARD_REMOVED" }
After you’ve successfully authenticated through telnet, you can connect to rtsp://192.168.42.1:554/live
with VLC (or similar) to see the live view of the camera.
This doesn’t always work. The only 100% way I know is to first connect with the official app.
vf_start/stop
Fired after start_photo_capture
Best guess: stop live view
{ "msg_id": 7, "type": "vf_stop" }
Fired after photo_taken
Best guess: start live view
{ "msg_id": 7, "type": "vf_start" }
vf
== “viewfinder” ?
This message has been observed when taking out the battery with the cam on USB power, but I haven’t been able to reproduce it so far.
{ "msg_id": 7, "type": "battery_status" ,"param":"0"}
param
– observed values0
and -1
Some of my favourite free, modern-looking, easy to use, web-based email clients that anyone can install and use with their mail server (in no particular order).
Copyright © 2024 Bloggity Blog
Theme by Anders Noren — Up ↑