2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.playstation.internal;
15 import java.nio.ByteBuffer;
16 import java.nio.ByteOrder;
17 import java.nio.charset.StandardCharsets;
19 import org.eclipse.jdt.annotation.NonNullByDefault;
22 * The {@link PS4PacketHandler} is responsible for creating and parsing
23 * packets to / from the PS4.
25 * @author Fredrik Ahlström - Initial contribution
28 public class PS4PacketHandler {
30 private static final String APPLICATION_NAME = "openHAB PlayStation 4 Binding";
31 private static final String DEVICE_NAME = "openHAB Server";
33 private static final String OS_VERSION = "8.1.0";
34 private static final String DDP_VERSION = "device-discovery-protocol-version:00020020\n";
35 static final int REQ_VERSION = 0x20000;
37 private PS4PacketHandler() {
42 * Allocates a new ByteBuffer of exactly size.
44 * @param size The size of the packet.
45 * @param cmd The command to add to the packet.
46 * @return A ByteBuffer of exactly size number of bytes.
48 static ByteBuffer newPacketOfSize(int size, PS4Command cmd) {
49 ByteBuffer packet = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
50 packet.putInt(size).putInt(cmd.value);
55 * Allocates a new ByteBuffer of size aligned to be a multiple of 16 bytes.
57 * @param size The size of the data in the packet.
58 * @param cmd The command to add to the packet.
59 * @return A ByteBuffer aligned to 16 byte size.
61 private static ByteBuffer newPacketForEncryption(int size, PS4Command cmd) {
62 int realSize = (((size + 15) >> 4) << 4);
63 ByteBuffer packet = ByteBuffer.allocate(realSize).order(ByteOrder.LITTLE_ENDIAN);
64 packet.putInt(size).putInt(cmd.value);
68 static byte[] makeSearchPacket() {
69 StringBuilder packet = new StringBuilder("SRCH * HTTP/1.1\n");
70 packet.append(DDP_VERSION);
71 return packet.toString().getBytes(StandardCharsets.UTF_8);
75 * A packet to start up the PS4 from standby mode.
77 * @param userCredential A 64 character long hex string.
78 * @return A wake-up packet.
80 static byte[] makeWakeupPacket(String userCredential) {
81 StringBuilder packet = new StringBuilder("WAKEUP * HTTP/1.1\n");
82 packet.append("client-type:a\n"); // i or a
83 packet.append("auth-type:C\n");
84 packet.append("model:a\n");
85 packet.append("app-type:g\n"); // c or g
86 packet.append("user-credential:" + userCredential + "\n");
87 packet.append(DDP_VERSION);
88 return packet.toString().getBytes(StandardCharsets.UTF_8);
92 * A packet to start up communication with the PS4.
94 * @param userCredential A 64 character long hex string
95 * @return A launch packet.
97 static byte[] makeLaunchPacket(String userCredential) {
98 StringBuilder packet = new StringBuilder("LAUNCH * HTTP/1.1\n");
99 packet.append("user-credential:" + userCredential + "\n");
100 packet.append(DDP_VERSION);
101 return packet.toString().getBytes(StandardCharsets.UTF_8);
104 static ByteBuffer makeHelloPacket() {
105 ByteBuffer packet = newPacketOfSize(28, PS4Command.HELLO_REQ);
106 packet.putInt(REQ_VERSION);
107 packet.put(new byte[16]); // Seed = 16 bytes
113 * Make a login packet, also used when pairing the device to the PS4.
115 * @param userCredential
120 static ByteBuffer makeLoginPacket(String userCredential, String passCode, String pairingCode) {
121 ByteBuffer packet = newPacketForEncryption(16 + 64 + 256 + 16 + 16 + 16, PS4Command.LOGIN_REQ);
122 if (passCode.length() == 4) {
123 packet.put(passCode.getBytes(), 0, 4); // Pass-code
126 packet.putInt(0x0F00); // Magic number (was 0x0201 before).
127 if (userCredential.length() == 64) {
128 packet.put(userCredential.getBytes(StandardCharsets.US_ASCII), 0, 64);
130 packet.position(16 + 64);
131 packet.put(APPLICATION_NAME.getBytes(StandardCharsets.UTF_8)); // app_label
132 packet.position(16 + 64 + 256);
133 packet.put(OS_VERSION.getBytes()); // os_version
134 packet.position(16 + 64 + 256 + 16);
135 packet.put(DEVICE_NAME.getBytes(StandardCharsets.UTF_8)); // Model, name of paired unit, shown on the PS4
136 // in the settings view.
137 packet.position(16 + 64 + 256 + 16 + 16);
138 if (pairingCode.length() == 8) {
139 packet.put(pairingCode.getBytes(), 0, 8); // Pairing-code
145 * Required for getting HPPTd status. Tell the PS4 who we are?
147 * @param clientID Example: "com.playstation.mobile2ndscreen".
148 * @param clientVersion Example: "18.9.3"
149 * @return A ClientID packet.
151 static ByteBuffer makeClientIDPacket(String clientID, String clientVersion) {
152 ByteBuffer packet = newPacketForEncryption(8 + 128 + 32, PS4Command.CLIENT_IDENTITY_REQ);
153 int length = clientID.length();
155 packet.put(clientID.getBytes(StandardCharsets.UTF_8));
157 packet.position(8 + 128);
158 length = clientVersion.length();
160 packet.put(clientVersion.getBytes(StandardCharsets.UTF_8));
166 * Ask for PS4 status.
168 * @param status Can be one of 0 or 1?
169 * @return A ServerStatus packet.
171 static ByteBuffer makeStatusPacket(int status) {
172 ByteBuffer packet = newPacketForEncryption(12, PS4Command.STATUS_REQ);
173 packet.putInt(status); // status
178 * Makes a packet that puts the PS4 in standby mode.
180 * @return A standby-packet.
182 static ByteBuffer makeStandbyPacket() {
183 return newPacketForEncryption(8, PS4Command.STANDBY_REQ);
187 * Tries to start an application on the PS4.
189 * @param applicationId The ID of the application.
190 * @return An appStart-packet
192 static ByteBuffer makeApplicationPacket(String applicationId) {
193 ByteBuffer packet = newPacketForEncryption(8 + 16, PS4Command.APP_START_REQ);
194 packet.put(applicationId.getBytes(StandardCharsets.UTF_8)); // Application Id (CUSAxxxxx)
199 * Makes a packet that closes down the connection with the PS4.
201 * @return A ByeBye-packet.
203 static ByteBuffer makeByebyePacket() {
204 return newPacketForEncryption(8, PS4Command.BYEBYE_REQ);
208 * This doesn't seem to do anything?
210 * @return A logout-packet.
212 static ByteBuffer makeLogoutPacket() {
213 return newPacketForEncryption(8, PS4Command.LOGOUT_REQ);
218 * @return A screenshot-packet?
220 static ByteBuffer makeScreenShotPacket() {
221 ByteBuffer packet = newPacketForEncryption(12, PS4Command.SCREEN_SHOT_REQ);
226 static String parseHTTPdPacket(ByteBuffer buffer) {
228 int status = buffer.getInt(8);
229 int port = buffer.getInt(12);
230 int option = buffer.getInt(16);
231 return String.format("status:%d, port:%d, option:%08x.", status, port, option);
235 * Tell the PS4 that we want to get info about the OnScreenKeyboard.
237 * @return An OSKStartPacket.
239 static ByteBuffer makeOSKStartPacket() {
240 return newPacketForEncryption(8, PS4Command.OSK_START_REQ);
244 * Send text to the OSK on the PS4. Replaces all the text as it is now.
247 * @return An OSKStringChangePacket.
249 static ByteBuffer makeOSKStringChangePacket(String text) {
250 byte[] chars = text.getBytes(StandardCharsets.UTF_16LE);
251 ByteBuffer packet = newPacketForEncryption(28 + chars.length, PS4Command.OSK_CHANGE_STRING_REQ);
252 packet.putInt(text.length()); // preEditIndex
253 packet.putInt(0); // preEditLength
254 packet.putInt(text.length()); // caretIndex
255 packet.putInt(0); // editIndex
256 packet.putInt(0); // editLength
262 * Parses out the text from an OSKStringChange-packet.
264 * @param buffer The received packet from the PS4.
265 * @return The text in the packet.
267 static String parseOSKStringChangePacket(ByteBuffer buffer) {
269 int length = buffer.getInt() - 28;
270 byte[] chars = new byte[length];
273 return new String(chars, StandardCharsets.UTF_16LE);
278 * @param command 0 = return, 1 = close.
281 static ByteBuffer makeOSKControlPacket(int command) {
282 ByteBuffer packet = newPacketForEncryption(12, PS4Command.OSK_CONTROL_REQ);
283 packet.putInt(command);
287 static ByteBuffer makeRemoteControlPacket(int pushedKey) {
288 ByteBuffer packet = newPacketForEncryption(16, PS4Command.REMOTE_CONTROL_REQ);
289 packet.putInt(pushedKey);
290 packet.putInt(0); // HoldTime in milliseconds
299 static ByteBuffer makeCommentViewerStart(int i) {
300 ByteBuffer packet = newPacketForEncryption(12, PS4Command.COMMENT_VIEWER_START_REQ);
307 * @param type Can be 5?
308 * @param info If type is 5 only check bit 0.
311 static ByteBuffer makeCommentViewerEvent(int type, int info) {
312 ByteBuffer packet = newPacketForEncryption(16, PS4Command.COMMENT_VIEWER_EVENT);
318 static ByteBuffer makeCommentViewerSendPacket(int i, String text) {
319 byte[] chars = (text.length() > 60 ? text.substring(0, 60) : text).getBytes(StandardCharsets.UTF_8);
320 ByteBuffer packet = newPacketForEncryption(12 + chars.length, PS4Command.OSK_CHANGE_STRING_REQ);