Skip to content

Commit 008d6ef

Browse files
committedJul 9, 2021
Initial commit of code
0 parents  commit 008d6ef

20 files changed

+3863
-0
lines changed
 

‎LICENSE

+674
Large diffs are not rendered by default.

‎README.md

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# phev2mqtt
2+
3+
Library and utility to interact with a Mitsubishi Outlander PHEV via the Wifi
4+
remote control protocol.
5+
6+
Inspired by https://github.com/phev-remote/ but written entirely in Go.
7+
8+
For further information, read the [protocol documentation](protocol/README.md).
9+
10+
Tested against a MY18 vehicle.
11+
12+
## Supported functionality
13+
14+
* MQTT proxy to Phev
15+
* Connect to Phev and sniff messages
16+
* Decode raw messages from file or command line
17+
* Decode and replay a connection from a PCAP (Wireshark) sniff.
18+
* *Only tested on a MY18 Phev*
19+
20+
## Requirements
21+
22+
* Go compiler
23+
* To connect, a previously registered connection to a phone/tablet.
24+
* This library doesnt yet support client registration.
25+
26+
## Licence, etc
27+
28+
Licenced under the GPLv2.
29+
30+
Copyright 2021 Ben Buxton <bbuxton@gmail.com>
31+
32+
Contributions and PRs are welcome.
33+
34+
## Getting started.
35+
36+
### Compiling
37+
38+
#### Install Go
39+
40+
* Download and install the latest [Go compiler](https://golang.org/dl/)
41+
* Your distro packager may have an old version
42+
* For raspbian choose the ARMv6 release
43+
#### Install PCAP dev libraries
44+
45+
* Ensure you have install the libpcap-dev package
46+
47+
#### Download, extract, and compile phev2mqtt
48+
49+
* Download the phev2mqtt archive
50+
* Extract it
51+
* Go into its the top level directory run *go build*
52+
* Verify it runs with *./phev2mqtt -h*
53+
54+
### Connecting to the vehicle.
55+
56+
As the program does not (yet) support client registration, you will first need to
57+
register a phone/tablet to the car. Follow the [Mitsubishi instructions](https://www.mitsubishi-motors.com/en/products/outlander_phev/app/remote/)
58+
and register the phone app to the car. You will need the Wifi credentials provided
59+
with the car.
60+
61+
Next, find the MAC address of the client. On your phone/table, go to Wifi settings,
62+
search for the car SSID and find the MAC address used. On Android this will likely
63+
be a randomised address. Note this address down.
64+
65+
On your computer running the phev2mqtt tools, configure a new Wifi connection to the
66+
car's SSID, and it's also essential to set the Wifi mac address to the client MAC address
67+
you noted above. Poke around online for how to do this for your system.
68+
69+
Once connected to the car, you can sniff for messages by running *phev2mqtt client watch*.
70+
The phone client needs to be disconnected for this to work.
71+
You'll see a bunch of data go by - some of those will be decoded into readable
72+
messages such as charge and AC status.
73+
74+
### MQTT Gateway
75+
76+
The primary feature of this code is to run as a proxy between the car and
77+
MQTT. Registers with car status are sent to MQTT, both as raw register
78+
values and decoded functional values.
79+
80+
Start the MQTT gateway with:
81+
82+
`./phev2mqtt client mqtt --mqtt_server tcp://<your_mqtt_address:1883/ [--mqtt_username <mqtt_username>] [--mqtt_password <mqtt_password>]`
83+
84+
Topics are as follows:
85+
86+
| Topic/prefix | Direction | Description |
87+
|---|---|---|
88+
| phev/register/[register] | Out | Raw values of each register, as hex strings |
89+
| phev/available | Out | Wifi connection status to car. *online* or *offline* |
90+
| phev/battery/level | Out | Current drive battery level as a percent |
91+
| phev/ac/status | Out | Whether the car AC is on |
92+
| phev/ac/mode | Out | Mode of the AC, if on. *cool*, *heat*, *windscreen* |
93+
| phev/charge/charging | Out | Whether the battery is charging. *on* or *off* |
94+
| phev/charge/remaining | Out | Minutes left, if charging. |
95+
| phev/door/locked | Out | Whether the car is locked. *on* or *off* |
96+
| phev/vim | Out | Discovered VIN of the car |
97+
| phev/registrations | Out | Number of wifi clients registered to the car |
98+
| phev/set/register/[register] | In | Set register 0x[register] to value 0x[payload] |
99+
| phev/set/parkinglights | In | Set parking lights *on* or *off* |
100+
| phev/set/headlights | In | Set head lights *on* or *off* |
101+
| phev/set/cancelchargetimer | In | Cancel charge timer (any payload) |
102+
103+
104+
### Sniffing the official client
105+
106+
Further development of this library can be done with a packet dump of the official
107+
Mistubishi app.
108+
109+
A number of sniffer apps for phones are available for this. Two that the author have
110+
used are *Packet Capture* and *PCAP Remote*. These do not require root access, yet
111+
can successfully sniff the traffic into PCAP files for further analysis.
112+
113+
*Packet Capture* can save the PCAP files to your local phone storage which you can
114+
then extract off the phone.
115+
116+
*PCAP Remote* is a little more involved, but allows for live sniffing of the traffic.
117+
118+
Once you have downloaded the PCAP file(s) from the phone, you can analyse them with
119+
the command *phev2mqtt decode pcap <filename>*. Adjust the verbosity level (-v)
120+
between 'info', 'debug' and 'trace' for more details.
121+
122+
Additionally, the flag '--latency' will use the PCAP packet timestamps to decode
123+
the packets with original timings which can help pinpoint app events.
124+
125+

‎client/client.go

+301
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
// Package client implements a client for communicating with a Mitsubishi
2+
// Outlander Phev.
3+
package client
4+
5+
import (
6+
"encoding/hex"
7+
"fmt"
8+
log "github.com/sirupsen/logrus"
9+
"net"
10+
"sync"
11+
"time"
12+
13+
"github.com/buxtronix/phev2mqtt/protocol"
14+
)
15+
16+
const DefaultAddress = "192.168.8.46:8080"
17+
18+
// A Listener is for communicating messages from the vehicle to
19+
// interested clients.
20+
type Listener struct {
21+
// C has received messages.
22+
C chan *protocol.PhevMessage
23+
stop bool
24+
}
25+
26+
func (l *Listener) start() {
27+
l.stop = false
28+
l.C = make(chan *protocol.PhevMessage, 5)
29+
}
30+
31+
func (l *Listener) Stop() {
32+
l.stop = true
33+
}
34+
35+
func (l *Listener) send(m *protocol.PhevMessage) {
36+
select {
37+
case l.C <- m:
38+
default:
39+
log.Debug("%PHEV_RECV_LISTENER% message not sent")
40+
}
41+
}
42+
43+
func (l *Listener) ProcessStop() bool {
44+
if l.stop {
45+
close(l.C)
46+
l.stop = false
47+
return true
48+
}
49+
return false
50+
}
51+
52+
// A Client is a TCP client to a Phev.
53+
type Client struct {
54+
// Recv is a channel where incoming messages from the Phev are sent.
55+
Recv chan *protocol.PhevMessage
56+
// Send is a channel to send messages to the Phev.
57+
Send chan *protocol.PhevMessage
58+
59+
listeners []*Listener
60+
lMu sync.Mutex
61+
62+
address string
63+
conn net.Conn
64+
started chan struct{}
65+
66+
closed bool
67+
}
68+
69+
// An Option configures the client.
70+
type Option func(c *Client)
71+
72+
// AddressOption configures the address to the Phev.
73+
func AddressOption(address string) func(*Client) {
74+
return func(c *Client) {
75+
c.address = address
76+
}
77+
}
78+
79+
// New returns a new client, not yet connected.
80+
func New(opts ...Option) (*Client, error) {
81+
cl := &Client{
82+
Recv: make(chan *protocol.PhevMessage, 5),
83+
Send: make(chan *protocol.PhevMessage, 5),
84+
started: make(chan struct{}, 2),
85+
listeners: []*Listener{},
86+
address: DefaultAddress,
87+
}
88+
for _, o := range opts {
89+
o(cl)
90+
}
91+
return cl, nil
92+
}
93+
94+
// Create and return a new Listener.
95+
func (c *Client) AddListener() *Listener {
96+
c.lMu.Lock()
97+
defer c.lMu.Unlock()
98+
l := &Listener{}
99+
l.start()
100+
c.listeners = append(c.listeners, l)
101+
return l
102+
}
103+
104+
func (c *Client) RemoveListener(l *Listener) {
105+
newL := []*Listener{}
106+
c.lMu.Lock()
107+
defer c.lMu.Unlock()
108+
for _, lis := range c.listeners {
109+
if lis != l {
110+
newL = append(newL, lis)
111+
}
112+
}
113+
c.listeners = newL
114+
}
115+
116+
// Close closes the client.
117+
func (c *Client) Close() error {
118+
c.closed = true
119+
if c.conn == nil {
120+
return nil
121+
}
122+
return c.conn.Close()
123+
}
124+
125+
// Connect connects to the Phev.
126+
func (c *Client) Connect() error {
127+
conn, err := net.Dial("tcp", c.address)
128+
if err != nil {
129+
return err
130+
}
131+
log.Info("%PHEV_TCP_CONNECTED%")
132+
c.closed = false
133+
c.conn = conn
134+
go c.reader()
135+
go c.writer()
136+
go c.manage()
137+
138+
return nil
139+
}
140+
141+
var startTimeout = 20 * time.Second
142+
143+
// Start waits for the client to start.
144+
func (c *Client) Start() error {
145+
log.Debug("%%PHEV_START_AWAIT%%")
146+
startTimer := time.After(startTimeout)
147+
for {
148+
select {
149+
case _, ok := <-c.started:
150+
if !ok {
151+
log.Debug("%%PHEV_START_CLOSED%%")
152+
return fmt.Errorf("receiver closed before getting start request")
153+
}
154+
log.Debug("%%PHEV_START_DONE%%")
155+
case <-startTimer:
156+
log.Debug("%%PHEV_START_TIMEOUT%%")
157+
return fmt.Errorf("timed out waiting for start")
158+
}
159+
return nil
160+
}
161+
}
162+
163+
// SetRegister sets a register on the car.
164+
func (c *Client) SetRegister(register byte, value []byte) error {
165+
setRegister := func(xor byte) {
166+
c.Send <- &protocol.PhevMessage{
167+
Type: protocol.CmdOutSend,
168+
Ack: protocol.Request,
169+
Register: register,
170+
Data: value,
171+
Xor: xor,
172+
}
173+
}
174+
xor := byte(0)
175+
timer := time.After(10 * time.Second)
176+
l := c.AddListener()
177+
defer c.RemoveListener(l)
178+
SETREG:
179+
setRegister(xor)
180+
for {
181+
select {
182+
case <-timer:
183+
return fmt.Errorf("timed out attempting to set register %02x", register)
184+
case msg, ok := <-l.C:
185+
if !ok {
186+
return fmt.Errorf("listener channel closed")
187+
}
188+
if msg.Type == protocol.CmdInBadEncoding {
189+
xor = msg.Data[0]
190+
goto SETREG
191+
}
192+
if msg.Type == protocol.CmdInResp && msg.Ack == protocol.Ack && msg.Register == register {
193+
return nil
194+
}
195+
196+
}
197+
}
198+
}
199+
200+
func (c *Client) nextRecvMsg(deadline time.Time) (*protocol.PhevMessage, error) {
201+
timer := time.After(deadline.Sub(time.Now()))
202+
for {
203+
select {
204+
case <-timer:
205+
return nil, fmt.Errorf("timed out waiting for message")
206+
case m, ok := <-c.Recv:
207+
if !ok {
208+
return nil, fmt.Errorf("error: receive channel closed")
209+
}
210+
return m, nil
211+
}
212+
}
213+
}
214+
215+
// manages the connection, handling control messages.
216+
func (c *Client) manage() {
217+
ml := c.AddListener()
218+
defer ml.Stop()
219+
for m := range ml.C {
220+
switch m.Type {
221+
case protocol.CmdInStartResp:
222+
c.Send <- &protocol.PhevMessage{
223+
Type: protocol.CmdOutPingReq,
224+
Ack: protocol.Request,
225+
Register: 0xa,
226+
Data: []byte{0x0},
227+
Xor: m.Xor,
228+
}
229+
case protocol.CmdInMy18StartReq:
230+
c.Send <- &protocol.PhevMessage{
231+
Type: protocol.CmdOutMy18StartResp,
232+
Register: 0x1,
233+
Ack: protocol.Ack,
234+
Xor: m.Xor,
235+
Data: []byte{0x0},
236+
}
237+
log.Debug("%%PHEV_START_RECV%%")
238+
c.started <- struct{}{}
239+
}
240+
}
241+
close(c.started)
242+
log.Debug("%PHEV_MANAGER_END%%")
243+
}
244+
245+
func (c *Client) reader() {
246+
for {
247+
c.conn.(*net.TCPConn).SetReadDeadline(time.Now().Add(30 * time.Second))
248+
data := make([]byte, 4096)
249+
n, err := c.conn.Read(data)
250+
if err != nil {
251+
if !c.closed {
252+
log.Debug("%%PHEV_TCP_READER_ERROR%%: ", err)
253+
}
254+
log.Debug("%PHEV_TCP_READER_CLOSE%")
255+
c.conn.Close()
256+
close(c.Recv)
257+
c.lMu.Lock()
258+
for _, l := range c.listeners {
259+
l.Stop()
260+
}
261+
c.lMu.Unlock()
262+
return
263+
}
264+
log.Tracef("%%PHEV_TCP_RECV_DATA%%: %s", hex.EncodeToString(data[:n]))
265+
messages := protocol.NewFromBytes(data[:n])
266+
for _, m := range messages {
267+
log.Debugf("%%PHEV_TCP_RECV_MSG%%: [%02x] %s", m.Xor, m.ShortForm())
268+
c.lMu.Lock()
269+
for _, l := range c.listeners {
270+
l.send(m)
271+
}
272+
c.lMu.Unlock()
273+
c.Recv <- m
274+
}
275+
}
276+
}
277+
278+
func (c *Client) writer() {
279+
for {
280+
select {
281+
case msg, ok := <-c.Send:
282+
if !ok {
283+
log.Debug("%PHEV_TCP_WRITER_CLOSE%")
284+
c.conn.Close()
285+
return
286+
}
287+
log.Debugf("%%PHEV_TCP_SEND_MSG%%: [%02x] %s", msg.Xor, msg.ShortForm())
288+
data := msg.EncodeToBytes()
289+
log.Tracef("%%PHEV_TCP_SEND_DATA%%: %s", hex.EncodeToString(data))
290+
c.conn.(*net.TCPConn).SetWriteDeadline(time.Now().Add(15 * time.Second))
291+
if _, err := c.conn.Write(data); err != nil {
292+
if !c.closed {
293+
log.Errorf("%%PHEV_TCP_WRITER_ERROR%%: %v", err)
294+
}
295+
log.Debug("%PHEV_TCP_WRITER_CLOSE%")
296+
c.conn.Close()
297+
return
298+
}
299+
}
300+
}
301+
}

‎cmd/client.go

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
Copyright © 2021 Ben Buxton <bbuxton@gmail.com>
3+
4+
This program is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
This program is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
package cmd
18+
19+
import (
20+
"fmt"
21+
22+
"github.com/spf13/cobra"
23+
)
24+
25+
// clientCmd represents the client command
26+
var clientCmd = &cobra.Command{
27+
Use: "client",
28+
Short: "Client to connect to the Phev",
29+
Long: ` `,
30+
Run: func(cmd *cobra.Command, args []string) {
31+
fmt.Println("Please choose a sub-command.")
32+
cmd.Help()
33+
},
34+
}
35+
36+
func init() {
37+
rootCmd.AddCommand(clientCmd)
38+
39+
// Here you will define your flags and configuration settings.
40+
41+
// Cobra supports Persistent Flags which will work for this command
42+
// and all subcommands, e.g.:
43+
// clientCmd.PersistentFlags().String("foo", "", "A help for foo")
44+
clientCmd.PersistentFlags().String("address", "192.168.8.46:8080", "Address to connect to")
45+
46+
// Cobra supports local flags which will only run when this command
47+
// is called directly, e.g.:
48+
// clientCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
49+
}

‎cmd/decode.go

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
Copyright © 2021 Ben Buxton <bbuxton@gmail.com>
3+
4+
This program is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
This program is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
package cmd
18+
19+
import (
20+
"fmt"
21+
"github.com/spf13/cobra"
22+
)
23+
24+
// decodeCmd represents the decode command
25+
var decodeCmd = &cobra.Command{
26+
Use: "decode",
27+
Short: "Commands to decode Phev messages",
28+
Long: `Commands for decoding messages to a from a Phev `,
29+
Run: func(cmd *cobra.Command, args []string) {
30+
fmt.Println("Please specify a sub-command")
31+
cmd.Help()
32+
},
33+
}
34+
35+
func init() {
36+
rootCmd.AddCommand(decodeCmd)
37+
38+
// Here you will define your flags and configuration settings.
39+
40+
// Cobra supports Persistent Flags which will work for this command
41+
// and all subcommands, e.g.:
42+
// decodeCmd.PersistentFlags().String("foo", "", "A help for foo")
43+
44+
// Cobra supports local flags which will only run when this command
45+
// is called directly, e.g.:
46+
// decodeCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
47+
}

‎cmd/file.go

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
Copyright © 2021 Ben Buxton <bbuxton@gmail.com>
3+
4+
This program is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
This program is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
package cmd
18+
19+
import (
20+
"encoding/hex"
21+
"os"
22+
"strings"
23+
24+
"github.com/buxtronix/phev2mqtt/protocol"
25+
log "github.com/sirupsen/logrus"
26+
"github.com/spf13/cobra"
27+
)
28+
29+
// fileCmd represents the file command
30+
var fileCmd = &cobra.Command{
31+
Use: "file",
32+
Short: "Decode Phev messages from a file",
33+
Long: `A longer description that spans multiple lines and likely contains examples
34+
and usage of using your command. For example:.`,
35+
Args: cobra.ExactArgs(1),
36+
Run: func(cmd *cobra.Command, args []string) {
37+
if len(args) < 1 {
38+
log.Errorf("Missing: filename")
39+
return
40+
}
41+
f, err := os.Open(args[0])
42+
if err != nil {
43+
panic(err)
44+
}
45+
defer f.Close()
46+
47+
inData := make([]byte, 100000)
48+
n, err := f.Read(inData)
49+
if err != nil {
50+
panic(err)
51+
}
52+
dat := strings.Replace(string(inData[:n]), "\n", "", -1)
53+
binData, err := hex.DecodeString(dat)
54+
if err != nil {
55+
panic(err)
56+
}
57+
for _, msg := range protocol.NewFromBytes(binData) {
58+
log.Debug(hex.EncodeToString(msg.Original))
59+
log.Infof("%s", msg.ShortForm())
60+
}
61+
},
62+
}
63+
64+
func init() {
65+
decodeCmd.AddCommand(fileCmd)
66+
67+
// Here you will define your flags and configuration settings.
68+
69+
// Cobra supports Persistent Flags which will work for this command
70+
// and all subcommands, e.g.:
71+
// fileCmd.PersistentFlags().String("foo", "", "A help for foo")
72+
73+
// Cobra supports local flags which will only run when this command
74+
// is called directly, e.g.:
75+
// fileCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
76+
}

‎cmd/hex.go

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
Copyright © 2021 Ben Buxton <bbuxton@gmail.com>
3+
4+
This program is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
This program is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
package cmd
18+
19+
import (
20+
"encoding/hex"
21+
"github.com/buxtronix/phev2mqtt/protocol"
22+
log "github.com/sirupsen/logrus"
23+
"github.com/spf13/cobra"
24+
)
25+
26+
// hexCmd represents the hex command
27+
var hexCmd = &cobra.Command{
28+
Use: "hex",
29+
Short: "Decode provided raw hex messages.",
30+
Long: `Decodes raw messages provided as arguments. Messages
31+
should be in hex format, e;g 'dc2b2f762f7f'.`,
32+
Args: cobra.MinimumNArgs(1),
33+
Run: func(cmd *cobra.Command, args []string) {
34+
for _, arg := range args {
35+
data, err := hex.DecodeString(arg)
36+
if err != nil {
37+
log.Errorf("Not a valid hex string [%s]: %v", arg, err)
38+
continue
39+
}
40+
for _, msg := range protocol.NewFromBytes(data) {
41+
log.Debug(hex.EncodeToString(msg.Original))
42+
log.Infof("%s", msg.ShortForm())
43+
}
44+
}
45+
},
46+
}
47+
48+
func init() {
49+
decodeCmd.AddCommand(hexCmd)
50+
51+
// Here you will define your flags and configuration settings.
52+
53+
// Cobra supports Persistent Flags which will work for this command
54+
// and all subcommands, e.g.:
55+
// hexCmd.PersistentFlags().String("foo", "", "A help for foo")
56+
57+
// Cobra supports local flags which will only run when this command
58+
// is called directly, e.g.:
59+
// hexCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
60+
}

‎cmd/mqtt.go

+286
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
/*
2+
Copyright © 2021 Ben Buxton <bbuxton@gmail.com>
3+
4+
This program is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
This program is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
package cmd
18+
19+
import (
20+
"encoding/hex"
21+
"fmt"
22+
"github.com/buxtronix/phev2mqtt/client"
23+
"github.com/buxtronix/phev2mqtt/protocol"
24+
"github.com/spf13/cobra"
25+
"strings"
26+
"time"
27+
28+
mqtt "github.com/eclipse/paho.mqtt.golang"
29+
log "github.com/sirupsen/logrus"
30+
)
31+
32+
// mqttCmd represents the mqtt command
33+
var mqttCmd = &cobra.Command{
34+
Use: "mqtt",
35+
Short: "Start an MQTT bridge.",
36+
Long: `A longer description that spans multiple lines and likely contains examples
37+
and usage of using your command. For example:
38+
39+
Cobra is a CLI library for Go that empowers applications.
40+
This application is a tool to generate the needed files
41+
to quickly create a Cobra application.`,
42+
RunE: func(cmd *cobra.Command, args []string) error {
43+
mc := &mqttClient{}
44+
return mc.Run(cmd, args)
45+
},
46+
}
47+
48+
type mqttClient struct {
49+
client mqtt.Client
50+
options *mqtt.ClientOptions
51+
mqttData map[string]string
52+
updateInterval time.Duration
53+
54+
phev *client.Client
55+
56+
prefix string
57+
}
58+
59+
func (m *mqttClient) topic(topic string) string {
60+
return fmt.Sprintf("%s%s", m.prefix, topic)
61+
}
62+
63+
func (m *mqttClient) Run(cmd *cobra.Command, args []string) error {
64+
var err error
65+
66+
mqttServer, _ := cmd.Flags().GetString("mqtt_server")
67+
mqttUsername, _ := cmd.Flags().GetString("mqtt_username")
68+
mqttPassword, _ := cmd.Flags().GetString("mqtt_password")
69+
m.prefix, _ = cmd.Flags().GetString("mqtt_topic_prefix")
70+
71+
m.updateInterval, err = cmd.Flags().GetDuration("update_interval")
72+
if err != nil {
73+
return err
74+
}
75+
76+
m.options = mqtt.NewClientOptions().
77+
AddBroker(mqttServer).
78+
SetClientID("phev2mqtt").
79+
SetUsername(mqttUsername).
80+
SetPassword(mqttPassword).
81+
SetAutoReconnect(true).
82+
SetDefaultPublishHandler(m.handleIncomingMqtt)
83+
84+
m.client = mqtt.NewClient(m.options)
85+
if token := m.client.Connect(); token.Wait() && token.Error() != nil {
86+
return token.Error()
87+
}
88+
89+
if token := m.client.Subscribe(m.topic("/set/#"), 0, nil); token.Wait() && token.Error() != nil {
90+
return token.Error()
91+
}
92+
93+
m.mqttData = map[string]string{}
94+
95+
for {
96+
if err := m.handlePhev(cmd); err != nil {
97+
log.Error(err)
98+
}
99+
m.publish("/available", "offline")
100+
time.Sleep(15 * time.Second)
101+
}
102+
}
103+
104+
func (m *mqttClient) publish(topic, payload string) {
105+
if cache := m.mqttData[topic]; cache == payload {
106+
// Only publish new data.
107+
return
108+
}
109+
m.client.Publish(m.topic(topic), 0, false, payload)
110+
m.mqttData[topic] = payload
111+
}
112+
113+
func (m *mqttClient) handleIncomingMqtt(client mqtt.Client, msg mqtt.Message) {
114+
log.Infof("Topic: %s", msg.Topic())
115+
log.Infof("Payload: %s", msg.Payload())
116+
117+
topicParts := strings.Split(msg.Topic(), "/")
118+
if strings.HasPrefix(msg.Topic(), m.topic("/set/register/")) {
119+
if len(topicParts) != 4 {
120+
log.Infof("Bad topic format [%s]", msg.Topic())
121+
return
122+
}
123+
register, err := hex.DecodeString(topicParts[3])
124+
if err != nil {
125+
log.Infof("Bad register in topic [%s]: %v", msg.Topic(), err)
126+
return
127+
}
128+
data, err := hex.DecodeString(string(msg.Payload()))
129+
if err != nil {
130+
log.Infof("Bad payload [%s]: %v", msg.Payload(), err)
131+
return
132+
}
133+
if err := m.phev.SetRegister(register[0], data); err != nil {
134+
log.Infof("Error setting register %02x: %v", register[0], err)
135+
return
136+
}
137+
} else if msg.Topic() == m.topic("/set/parkinglights") {
138+
switch payload := string(msg.Payload()); payload {
139+
case "on":
140+
if err := m.phev.SetRegister(0xb, []byte{0x1}); err != nil {
141+
log.Infof("Error setting register 0xb: %v", err)
142+
return
143+
}
144+
case "off":
145+
if err := m.phev.SetRegister(0xb, []byte{0x2}); err != nil {
146+
log.Infof("Error setting register 0xb: %v", err)
147+
return
148+
}
149+
default:
150+
log.Errorf("Unsupported payload for %s: %s", msg.Topic(), payload)
151+
}
152+
} else if msg.Topic() == m.topic("/set/headlights") {
153+
switch payload := string(msg.Payload()); payload {
154+
case "on":
155+
if err := m.phev.SetRegister(0xa, []byte{0x1}); err != nil {
156+
log.Infof("Error setting register 0xb: %v", err)
157+
return
158+
}
159+
case "off":
160+
if err := m.phev.SetRegister(0xa, []byte{0x2}); err != nil {
161+
log.Infof("Error setting register 0xb: %v", err)
162+
return
163+
}
164+
default:
165+
log.Errorf("Unsupported payload for %s: %s", msg.Topic(), payload)
166+
}
167+
} else if msg.Topic() == m.topic("/set/cancelchargetimer") {
168+
if err := m.phev.SetRegister(0x17, []byte{0x1}); err != nil {
169+
log.Infof("Error setting register 0x17: %v", err)
170+
return
171+
}
172+
if err := m.phev.SetRegister(0x17, []byte{0x11}); err != nil {
173+
log.Infof("Error setting register 0x17: %v", err)
174+
return
175+
}
176+
} else {
177+
log.Errorf("Unknown topic from mqtt: %s", msg.Topic())
178+
}
179+
}
180+
181+
func (m *mqttClient) handlePhev(cmd *cobra.Command) error {
182+
var err error
183+
address, _ := cmd.Flags().GetString("address")
184+
m.phev, err = client.New(client.AddressOption(address))
185+
if err != nil {
186+
return err
187+
}
188+
189+
if err := m.phev.Connect(); err != nil {
190+
return err
191+
}
192+
193+
if err := m.phev.Start(); err != nil {
194+
return err
195+
}
196+
m.publish("/available", "online")
197+
198+
var encodingErrorCount = 0
199+
var lastEncodingError time.Time
200+
201+
updaterTicker := time.NewTicker(m.updateInterval)
202+
for {
203+
select {
204+
case <-updaterTicker.C:
205+
m.phev.SetRegister(0x6, []byte{0x3})
206+
case msg, ok := <-m.phev.Recv:
207+
if !ok {
208+
log.Infof("Connection closed.")
209+
updaterTicker.Stop()
210+
return fmt.Errorf("Connection closed.")
211+
}
212+
switch msg.Type {
213+
case protocol.CmdInBadEncoding:
214+
if time.Now().Sub(lastEncodingError) > 15*time.Second {
215+
encodingErrorCount = 0
216+
}
217+
if encodingErrorCount > 50 {
218+
m.phev.Close()
219+
updaterTicker.Stop()
220+
return fmt.Errorf("Disconnecting due to too many errors")
221+
}
222+
encodingErrorCount += 1
223+
lastEncodingError = time.Now()
224+
case protocol.CmdInResp:
225+
if msg.Ack != protocol.Request {
226+
break
227+
}
228+
m.publishRegister(msg)
229+
m.phev.Send <- &protocol.PhevMessage{
230+
Type: protocol.CmdOutSend,
231+
Register: msg.Register,
232+
Ack: protocol.Ack,
233+
Xor: msg.Xor,
234+
Data: []byte{0x0},
235+
}
236+
}
237+
}
238+
}
239+
}
240+
241+
var boolOnOff = map[bool]string{
242+
false: "off",
243+
true: "on",
244+
}
245+
246+
func (m *mqttClient) publishRegister(msg *protocol.PhevMessage) {
247+
dataStr := hex.EncodeToString(msg.Data)
248+
m.publish(fmt.Sprintf("/register/%02x", msg.Register), dataStr)
249+
switch reg := msg.Reg.(type) {
250+
case *protocol.RegisterVIN:
251+
m.publish("/vin", reg.VIN)
252+
m.publish("/registrations", fmt.Sprintf("%d", reg.Registrations))
253+
case *protocol.RegisterECUVersion:
254+
m.publish("/ecuversion", reg.Version)
255+
case *protocol.RegisterACMode:
256+
m.publish("/ac/mode", reg.Mode)
257+
case *protocol.RegisterACOperStatus:
258+
m.publish("/ac/status", boolOnOff[reg.Operating])
259+
case *protocol.RegisterChargeStatus:
260+
m.publish("/charge/charging", boolOnOff[reg.Charging])
261+
m.publish("/charge/remaining", fmt.Sprintf("%d", reg.Remaining))
262+
case *protocol.RegisterDoorStatus:
263+
m.publish("/door/locked", boolOnOff[reg.Locked])
264+
case *protocol.RegisterBatteryLevel:
265+
m.publish("/battery/level", fmt.Sprintf("%d", reg.Level))
266+
}
267+
}
268+
269+
func init() {
270+
clientCmd.AddCommand(mqttCmd)
271+
272+
// Here you will define your flags and configuration settings.
273+
274+
// Cobra supports Persistent Flags which will work for this command
275+
// and all subcommands, e.g.:
276+
// mqttCmd.PersistentFlags().String("foo", "", "A help for foo")
277+
278+
// Cobra supports local flags which will only run when this command
279+
// is called directly, e.g.:
280+
// mqttCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
281+
mqttCmd.Flags().String("mqtt_server", "tcp://127.0.0.1:1883", "Address of MQTT server")
282+
mqttCmd.Flags().String("mqtt_username", "", "Username to login to MQTT server")
283+
mqttCmd.Flags().String("mqtt_password", "", "Password to login to MQTT server")
284+
mqttCmd.Flags().String("mqtt_topic_prefix", "phev", "Prefix for MQTT topics")
285+
mqttCmd.Flags().Duration("update_interval", 5*time.Minute, "How often to request force updates")
286+
}

‎cmd/pcap.go

+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*
2+
Copyright © 2021 Ben Buxton <bbuxton@gmail.com>
3+
4+
This program is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
This program is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
package cmd
18+
19+
import (
20+
"encoding/hex"
21+
"strings"
22+
"time"
23+
24+
"github.com/buxtronix/phev2mqtt/protocol"
25+
"github.com/google/gopacket"
26+
"github.com/google/gopacket/layers"
27+
"github.com/google/gopacket/pcap"
28+
log "github.com/sirupsen/logrus"
29+
"github.com/spf13/cobra"
30+
)
31+
32+
// pcapCmd represents the pcap command
33+
var pcapCmd = &cobra.Command{
34+
Use: "pcap",
35+
Short: "Decode packets from a PCAP traffic dump",
36+
Long: `Reads packets from a PCAP file generated by a packet sniffer
37+
such as Wireshark.
38+
39+
The decoder filters TCP packets to and from port 8080.
40+
`,
41+
Args: cobra.ExactArgs(1),
42+
Run: func(cmd *cobra.Command, args []string) {
43+
handle, err := pcap.OpenOffline(args[0])
44+
if err != nil {
45+
log.Fatal(err)
46+
}
47+
defer handle.Close()
48+
49+
packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
50+
var currentTime time.Time
51+
pNum := 0
52+
for packet := range packetSource.Packets() {
53+
if m := packet.Metadata(); m != nil {
54+
if pNum > 0 {
55+
if r, _ := cmd.Flags().GetBool("latency"); r {
56+
time.Sleep(m.CaptureInfo.Timestamp.Sub(currentTime))
57+
}
58+
currentTime = m.CaptureInfo.Timestamp
59+
} else {
60+
currentTime = m.CaptureInfo.Timestamp
61+
}
62+
}
63+
pNum += 1
64+
decodePacket(cmd, packet)
65+
}
66+
},
67+
}
68+
69+
func decodePacket(cmd *cobra.Command, packet gopacket.Packet) {
70+
dir := "?"
71+
tcpLayer := packet.Layer(layers.LayerTypeTCP)
72+
if tcpLayer != nil {
73+
tcp, _ := tcpLayer.(*layers.TCP)
74+
if tcp.SrcPort == 8080 {
75+
dir = "in "
76+
} else if tcp.DstPort == 8080 {
77+
dir = "out "
78+
}
79+
if tcp.SYN && tcp.ACK {
80+
log.Infof("TCP-SYN-ACK\n")
81+
}
82+
if tcp.FIN {
83+
log.Infof("TCP-FIN\n")
84+
}
85+
if tcp.RST {
86+
log.Infof("TCP-RST\n")
87+
}
88+
}
89+
90+
d, _ := cmd.Flags().GetString("direction")
91+
if dir == "in " && d == "out" {
92+
return
93+
}
94+
if dir == "out " && d == "in" {
95+
return
96+
}
97+
98+
al := packet.ApplicationLayer()
99+
if al != nil {
100+
processPayload(cmd, al.Payload(), dir)
101+
}
102+
103+
if err := packet.ErrorLayer(); err != nil {
104+
log.Errorf("error decoding packet:", err)
105+
}
106+
}
107+
108+
func processPayload(cmd *cobra.Command, data []byte, dir string) {
109+
log.Tracef("%%PHEV_PCAP_RAW_%s%%: %s\n", strings.ToUpper(dir), hex.EncodeToString(data))
110+
msgs := protocol.NewFromBytes(data)
111+
for _, msg := range msgs {
112+
if r, _ := cmd.Flags().GetBool("registers"); r {
113+
handleRegisters(msg)
114+
return
115+
}
116+
log.Infof("%s [%02x] %s", dir, msg.Xor, msg.ShortForm())
117+
}
118+
}
119+
120+
var regs = map[byte]string{}
121+
122+
func handleRegisters(m *protocol.PhevMessage) {
123+
if m.Type == protocol.CmdInResp {
124+
data := hex.EncodeToString(m.Data)
125+
if d := regs[m.Register]; d != data {
126+
log.Infof("UPDATEREG 0x%02x: %s -> %s\n", m.Register, d, data)
127+
regs[m.Register] = data
128+
}
129+
}
130+
}
131+
132+
func init() {
133+
decodeCmd.AddCommand(pcapCmd)
134+
135+
// Here you will define your flags and configuration settings.
136+
137+
// Cobra supports Persistent Flags which will work for this command
138+
// and all subcommands, e.g.:
139+
// pcapCmd.PersistentFlags().String("foo", "", "A help for foo")
140+
141+
// Cobra supports local flags which will only run when this command
142+
// is called directly, e.g.:
143+
// pcapCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
144+
pcapCmd.Flags().StringP("direction", "d", "both", "Direction to decode")
145+
pcapCmd.Flags().BoolP("latency", "l", false, "Replay with original network latency")
146+
pcapCmd.Flags().BoolP("registers", "R", false, "Show register updates")
147+
}

‎cmd/root.go

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
Copyright © 2021 Ben Buxton <bbuxton@gmail.com>
3+
4+
This program is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
This program is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
package cmd
18+
19+
import (
20+
"fmt"
21+
log "github.com/sirupsen/logrus"
22+
"github.com/spf13/cobra"
23+
"os"
24+
25+
"github.com/spf13/viper"
26+
)
27+
28+
var (
29+
cfgFile string
30+
logLevel string
31+
)
32+
33+
// rootCmd represents the base command when called without any subcommands
34+
var rootCmd = &cobra.Command{
35+
Use: "phev2mqtt",
36+
Short: "A utility for communicating with a Mitsubishi PHEV",
37+
Long: `Phev tool.`,
38+
PersistentPreRun: func(cmd *cobra.Command, args []string) {
39+
level, err := log.ParseLevel(logLevel)
40+
if err != nil {
41+
panic(err)
42+
}
43+
log.SetLevel(level)
44+
},
45+
46+
// Uncomment the following line if your bare application
47+
// has an action associated with it:
48+
// Run: func(cmd *cobra.Command, args []string) { },
49+
}
50+
51+
// Execute adds all child commands to the root command and sets flags appropriately.
52+
// This is called by main.main(). It only needs to happen once to the rootCmd.
53+
func Execute() {
54+
cobra.CheckErr(rootCmd.Execute())
55+
}
56+
57+
func init() {
58+
cobra.OnInitialize(initConfig)
59+
60+
// Here you will define your flags and configuration settings.
61+
// Cobra supports persistent flags, which, if defined here,
62+
// will be global for your application.
63+
64+
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.phev2mqtt.yaml)")
65+
rootCmd.PersistentFlags().StringVarP(&logLevel, "verbosity", "v", "info", "logging level to use")
66+
67+
// Cobra also supports local flags, which will only run
68+
// when this action is called directly.
69+
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
70+
}
71+
72+
// initConfig reads in config file and ENV variables if set.
73+
func initConfig() {
74+
if cfgFile != "" {
75+
// Use config file from the flag.
76+
viper.SetConfigFile(cfgFile)
77+
} else {
78+
// Find home directory.
79+
home, err := os.UserHomeDir()
80+
cobra.CheckErr(err)
81+
82+
// Search config in home directory with name ".phev2mqtt" (without extension).
83+
viper.AddConfigPath(home)
84+
viper.SetConfigType("yaml")
85+
viper.SetConfigName(".phev2mqtt")
86+
}
87+
88+
viper.AutomaticEnv() // read in environment variables that match
89+
90+
// If a config file is found, read it in.
91+
if err := viper.ReadInConfig(); err == nil {
92+
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
93+
}
94+
}

‎cmd/set.go

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
Copyright © 2021 Ben Buxton <bbuxton@gmail.com>
3+
4+
This program is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
This program is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
package cmd
18+
19+
import (
20+
"encoding/hex"
21+
"strings"
22+
23+
"github.com/buxtronix/phev2mqtt/client"
24+
// log "github.com/sirupsen/logrus"
25+
"github.com/spf13/cobra"
26+
)
27+
28+
// setCmd represents the set command
29+
var setCmd = &cobra.Command{
30+
Use: "set",
31+
Short: "Set register value(s)",
32+
Args: cobra.MinimumNArgs(1),
33+
Long: `Send new values to the car to set register(s).
34+
35+
Each arg is of the form <register>:<value> - the given register is set to the
36+
given value. Each should be a hex string.
37+
38+
e.g:
39+
40+
0b:02 will set register 0b to the value 02
41+
42+
`,
43+
Run: runSet,
44+
}
45+
46+
type regValue struct {
47+
register byte
48+
value []byte
49+
}
50+
51+
func runSet(cmd *cobra.Command, args []string) {
52+
var register, value []byte
53+
var err error
54+
55+
setRegisters := []*regValue{}
56+
57+
for _, arg := range args {
58+
if vals := strings.Split(arg, ":"); len(vals) == 2 {
59+
register, err = hex.DecodeString(vals[0])
60+
if err != nil {
61+
panic(err)
62+
}
63+
value, err = hex.DecodeString(vals[1])
64+
if err != nil {
65+
panic(err)
66+
}
67+
setRegisters = append(setRegisters, &regValue{
68+
register: register[0], value: value,
69+
})
70+
}
71+
}
72+
73+
address, _ := cmd.Flags().GetString("address")
74+
cl, err := client.New(client.AddressOption(address))
75+
if err != nil {
76+
panic(err)
77+
}
78+
79+
if err := cl.Connect(); err != nil {
80+
panic(err)
81+
}
82+
83+
if err := cl.Start(); err != nil {
84+
panic(err)
85+
}
86+
87+
for _, reg := range setRegisters {
88+
if err := cl.SetRegister(reg.register, reg.value); err != nil {
89+
panic(err)
90+
}
91+
}
92+
93+
}
94+
95+
func init() {
96+
clientCmd.AddCommand(setCmd)
97+
98+
// Here you will define your flags and configuration settings.
99+
100+
// Cobra supports Persistent Flags which will work for this command
101+
// and all subcommands, e.g.:
102+
// registerCmd.PersistentFlags().String("foo", "", "A help for foo")
103+
104+
// Cobra supports local flags which will only run when this command
105+
// is called directly, e.g.:
106+
// registerCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
107+
}

‎cmd/watch.go

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
Copyright © 2021 Ben Buxton <bbuxton@gmail.com>
3+
4+
This program is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
This program is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
package cmd
18+
19+
import (
20+
"encoding/hex"
21+
"time"
22+
23+
"github.com/buxtronix/phev2mqtt/client"
24+
"github.com/buxtronix/phev2mqtt/protocol"
25+
log "github.com/sirupsen/logrus"
26+
"github.com/spf13/cobra"
27+
)
28+
29+
// watchCmd represents the watch command
30+
var watchCmd = &cobra.Command{
31+
Use: "watch",
32+
Short: "Connect to Phev and watch incoming updates",
33+
Long: `Connects to the Phev and watches for incoming register
34+
updates, displaying them in real-time.`,
35+
Run: Run,
36+
}
37+
38+
func Run(cmd *cobra.Command, args []string) {
39+
address, _ := cmd.Flags().GetString("address")
40+
cl, err := client.New(client.AddressOption(address))
41+
if err != nil {
42+
panic(err)
43+
}
44+
45+
if err := cl.Connect(); err != nil {
46+
panic(err)
47+
}
48+
49+
if err := cl.Start(); err != nil {
50+
panic(err)
51+
}
52+
53+
var registers = map[byte]string{}
54+
55+
for {
56+
select {
57+
case m, ok := <-cl.Recv:
58+
if !ok {
59+
log.Infof("Connection closed.")
60+
return
61+
}
62+
switch m.Type {
63+
case protocol.CmdInResp:
64+
dataStr := hex.EncodeToString(m.Data)
65+
if data := registers[m.Register]; data != dataStr {
66+
log.Infof("%%PHEV_REG_UPDATE%% %02x: %s -> %s", m.Register, data, dataStr)
67+
registers[m.Register] = dataStr
68+
if _, ok := m.Reg.(*protocol.RegisterGeneric); !ok {
69+
log.Infof("%%PHEV_REG_UPDATE%% %02x: [%s]", m.Register, m.Reg.String())
70+
}
71+
}
72+
cl.Send <- &protocol.PhevMessage{
73+
Type: protocol.CmdOutSend,
74+
Register: m.Register,
75+
Ack: protocol.Ack,
76+
Xor: m.Xor,
77+
Data: []byte{0x0},
78+
}
79+
}
80+
}
81+
}
82+
}
83+
84+
func init() {
85+
clientCmd.AddCommand(watchCmd)
86+
87+
// Here you will define your flags and configuration settings.
88+
89+
// Cobra supports Persistent Flags which will work for this command
90+
// and all subcommands, e.g.:
91+
// watchCmd.PersistentFlags().String("foo", "", "A help for foo")
92+
93+
// Cobra supports local flags which will only run when this command
94+
// is called directly, e.g.:
95+
// watchCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
96+
watchCmd.Flags().DurationP("wait", "w", 60*time.Second, "How long to hold connection open for")
97+
}

‎go.mod

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module github.com/buxtronix/phev2mqtt
2+
3+
go 1.16
4+
5+
require (
6+
github.com/d4l3k/messagediff v1.2.1 // indirect
7+
github.com/eclipse/paho.mqtt.golang v1.3.5
8+
github.com/google/gopacket v1.1.19
9+
github.com/sirupsen/logrus v1.8.1
10+
github.com/spf13/cobra v1.2.1
11+
github.com/spf13/viper v1.8.1
12+
gopkg.in/d4l3k/messagediff.v1 v1.2.1
13+
)

‎go.sum

+606
Large diffs are not rendered by default.

‎main.go

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
Copyright © 2021 Ben Buxton <bbuxton@gmail.com>
3+
4+
This program is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
This program is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
package main
18+
19+
import "github.com/buxtronix/phev2mqtt/cmd"
20+
21+
func main() {
22+
cmd.Execute()
23+
}

‎protocol/README.md

+325
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
# PHEV Remote protocol (MY18)
2+
3+
A brief description of the protocol used. Determined by analysis and reverse
4+
engineering, together the looking at the [phev-remote](https://github.com/phev-remote)
5+
code.
6+
7+
## Network transport
8+
9+
The client device connects to the car using an SSID with a default format
10+
of *REMOTExxxxxx* where *xxxxxx* is an arbitrary string.
11+
12+
The car assigns the client an IP address over DHCP, usually 192.168.8.47. The
13+
car itself is 192.168.8.46.
14+
15+
The client communicates to the car's address over TCP port 8080. Within the
16+
TCP connection, binary packets are used to exchange data.
17+
18+
## Packet format.
19+
20+
Each packet has the following format:
21+
22+
```
23+
| 1 byte | 1 byte | 1 byte | n bytes ... | 1 byte |
24+
| [Type] [Length] [Ack] [Data] [Checksum] |
25+
```
26+
27+
The fields are:
28+
29+
### Type
30+
31+
One byte.
32+
33+
The type of the packet. Most packet types have a corresponding response type.
34+
See below for known types.
35+
36+
### Length
37+
38+
One byte
39+
40+
The length of the packet, less two. For example a packet that contains two
41+
data bytes has a length field of 4.
42+
43+
### Ack
44+
45+
One byte
46+
47+
The acknowledgement field. Will be 0 for request packets, and 1 for packets
48+
acknowledging a request type.
49+
50+
### Data
51+
52+
Variable length
53+
54+
The data payload for the packet. Depends on the command. The length of this
55+
field is 4 less than the value in the *Length* field.
56+
57+
### Checksum
58+
59+
One byte
60+
61+
The checksum is a basic packet integrity check. To calculate its value, add
62+
up of all the previous bytes in the packet. The least significant octet is
63+
the checksum.
64+
65+
For example, given a packet of `f3040020`, the sum is 0x117, so the checksum
66+
is `0x17`. Thus the entire packet is `f304002017`.
67+
68+
## XOR encoding
69+
70+
Many of the packets on the wire are further encoded with a basic XOR scheme.
71+
72+
Each byte in the packet is XORed with a single byte value. The mechanism used
73+
to choose the value appears to be some rolling algorithm. Response packets
74+
from the client seem to use the same XOR value from the corresponding request packet.
75+
76+
Determining the XOR value to decode can be done by XORing the encoded packet
77+
with the value in the *Type* field. If the *Command* and *Checksum* are
78+
valid, then this is the correct Xor. If not, flip the last bit of thie *Type*
79+
value and try again.
80+
81+
When sending a command packet to the car, most packets must be encoded with a
82+
valid Xor value. Currently the algorithm to choose this is undetermined. However
83+
this can be worked around as the car will send back an error packet containing
84+
the expected Xor value. Just re-send the packet with this provided Xor.
85+
86+
## Packet types
87+
88+
Summary:
89+
90+
| Value | Name | Direction| Description |
91+
|------|-----|---|--|
92+
| 0xf3 | Ping Request | client -> car | Ping/Keepalive request |
93+
| 0x3f | Ping response| car -> client | Ping / Keepalive response |
94+
| 0x6f | Register changed | car -> client | Notify a register has been updated |
95+
| 0xf6 | Register update | client -> car | Client register change ack/set |
96+
| 0x5e | Init connection? | car -> client | Initialise connection? |
97+
| 0xe5 | Init ack? | client -> car | Ack connection init? |
98+
| 0x2f | ??? | car -> client | Unknown |
99+
| 0xbb | Bad Xor? | car -> client | Sent when XOR value is incorrect. |
100+
| 0xcc | Bad Xor? | car -> client | Sent when XOR value is incorrect. |
101+
102+
### Ping request (0xf3)
103+
104+
Format:
105+
106+
```
107+
[f3][04][00][<seq>][00][<cksum>]
108+
```
109+
110+
Seems to be a keepalive sent to the car from the client. The initial XOR seems to be 00
111+
until a 0x5e packet is received from the car. The `<seq>` increments up to 0x63 then
112+
overflows back to 0x0.
113+
114+
115+
### Ping response (03f)
116+
Format:
117+
118+
```
119+
[f3][04][01][<seq>][00][<cksum>]
120+
```
121+
122+
Response to a 0xf3 packet, sent from the car to the client. The `<seq>` matches the
123+
request, though the XOR seems to be chosen by the car and future register update packets
124+
match this XOR until another ping exchange.
125+
126+
127+
### Register changed (0x6f)
128+
Format:
129+
130+
```
131+
[6f][<len>][<ack>][<register>][<data>][<cksum>]
132+
```
133+
134+
Used to notify the client that a setting register has changed on the car.
135+
136+
If `<ack>` is zero, then indicates a notification of a register value. If `<ack>`
137+
is one, this is a response to a register setting change requested by the client.
138+
139+
The `<register>` is a one-byte value signifying the corresponding register number.
140+
141+
The `<data>` is variable length and dependent on the specific register.
142+
143+
Registers are described below.
144+
145+
146+
### Register update/ack (0xf6)
147+
148+
```
149+
[f6][<len>][<ack>][<register>][<data>][<cksum>]
150+
```
151+
152+
Used by the client to notify the car of either an ack of a received register update, or setting a new register value.
153+
154+
If `<ack>` is zero, indicates that a register value is to be changed. The `<register>` and `<value>` fields indicate the register and new value.
155+
156+
If `<ack>` is one, is a response to an update (above) received from the car. The `<register>` field is the register value being acked. The `<data>` field contains a single `0x0` byte.
157+
158+
159+
### Init connection (0x5e)
160+
```
161+
[5e][0c][00][<data>][<cksum>]
162+
```
163+
164+
Sent by the car after 10 initial ping/keepalive exchanges.
165+
166+
The `<data>` contents are 12 unknown bytes but seems to change without a known pattern.
167+
168+
169+
### Init ack (0xe5)
170+
```
171+
[e5][04][01][0100][<cksum>]
172+
```
173+
174+
Sent in response to a 0x5e packet. Always seems to be the same.
175+
176+
### Bad XOR (0xbb)
177+
```
178+
[bb][06][01][<unknown>][<exp>]
179+
```
180+
181+
Sent by the car if a received packet is encoded with the incorrect XOR value.
182+
183+
The meaning of the value in the `<unknown>` field is, well, unknown.
184+
185+
The `<exp>` field contains the XOR value which is expected. If the offending
186+
packet is reset using this value for the Xor, then it will likely be accepted.
187+
188+
This can be a workaround given the current unknown algorithm for generating
189+
XOR values.
190+
191+
## Registers
192+
193+
Registers contain the bulk of information on the state of the vehicle.
194+
195+
| Register | Name | Description |
196+
|--|--|--|
197+
|0x1 | ?? | |
198+
|0x2 | Battery warning | |
199+
|0x3 | ?? | |
200+
|0x4 | ?? | |
201+
|0x5 | ?? | |
202+
|0x6 | ?? | Similar to 0x15 |
203+
|0x7 | ?? | |
204+
|0xa | Head light state? | Write to set the head lights on |
205+
|0xb | Parking light state? | Write to set the parking lights on |
206+
|0xc | ?? | |
207+
|0xd | ?? | |
208+
|0xf | ?? | |
209+
|0x10 | AirCon State | |
210+
|0x11 | ?? | |
211+
|0x12 | TimeSync | |
212+
|0x14 | ?? | |
213+
|0x15 | VIN | |
214+
|0x16 | ?? | |
215+
|0x17 | Charge timer? | Write 0x1 then 0x11 to disable charge timer |
216+
|0x1a | ?? | |
217+
|0x1b | ?? | |
218+
|0x1c | AirCon Mode | |
219+
|0x1d | Battery Level | |
220+
|0x1e | ?? | |
221+
|0x1f | Charge State | |
222+
|0x21 | ?? | |
223+
|0x22 | ?? | |
224+
|0x23 | ?? | |
225+
|0x24 | Door Lock Status | |
226+
|0x25 | ?? | |
227+
|0x26 | ?? | |
228+
|0x27 | ?? | |
229+
|0x28 | ?? | |
230+
|0x29 | ?? | |
231+
|0x2c | ?? | |
232+
|0xc0 | ECU Version | |
233+
234+
### 0x02 - Battery warning
235+
236+
### 0x0a - Headlight status
237+
238+
### 0x0b - Parking light status
239+
240+
### 0x10 - Aircon status
241+
242+
3 bytes.
243+
244+
| Byte(s) | Description |
245+
|--|--|
246+
|0 | Unknown |
247+
| 1 | AC operating [0=off 1=on] |
248+
| 2 | Unknown |
249+
250+
### 0x12 - Car time sync
251+
252+
### 0x15 - Vin / registration state
253+
254+
Vin info and regstration status.
255+
256+
| Byte(s) | Description |
257+
|--|--|
258+
|0 | Unknown |
259+
|1-18 | VIN (ascii) |
260+
|19 | Number of registered clients |
261+
262+
263+
### 0x17 - Charge timer state
264+
265+
### 0x1c - Aircon mode
266+
267+
Single byte.
268+
269+
| Value | Description |
270+
|--|--|
271+
|0 | Unknown |
272+
|1 | Heating |
273+
|2 | Cooling |
274+
|3 | Windscreen |
275+
276+
### 0x1d - Drive battery level
277+
278+
```
279+
10000003
280+
```
281+
282+
| Byte(s) | Description |
283+
|--|--|
284+
|0 | Drive battery level % |
285+
| 1-3 | Unknown |
286+
287+
288+
### 0x1f - Charging status
289+
290+
| Byte(s) | Description |
291+
|--|--|
292+
|0 | Charge status [0=not charging 1=charging]|
293+
| 1-2 | Charge time remaining |
294+
295+
### 0x24 - Door / Lock status
296+
297+
```
298+
Byte 0
299+
|
300+
01000000000000000000
301+
|
302+
Byte 19
303+
```
304+
305+
Below shows what is represented by each byte. For door/boot/bonnet,
306+
the value is 1 if open, else 0.
307+
308+
1 - Lock status [1=locked 2=unlocked]
309+
310+
7 - Front Right Door
311+
312+
9 - Front Left Door
313+
314+
11 - Read Right Door
315+
316+
13 - Rear Left Door
317+
318+
15 - Boot / Trunk
319+
320+
17 - Bonnet / Hood
321+
322+
### 0xc0 - ECU Version string
323+
324+
A string with the software version of the ECU.
325+

‎protocol/message.go

+473
Large diffs are not rendered by default.

‎protocol/message_test.go

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package protocol
2+
3+
import (
4+
"encoding/hex"
5+
"gopkg.in/d4l3k/messagediff.v1"
6+
"testing"
7+
)
8+
9+
func TestDecodeEncodeBytes(t *testing.T) {
10+
tests := []struct {
11+
in string
12+
want *PhevMessage
13+
}{
14+
{
15+
in: "f60400060303",
16+
want: &PhevMessage{
17+
Type: 0xf6,
18+
Length: 0x6,
19+
Register: 0x6,
20+
Data: []byte{0x3},
21+
Checksum: 0x3,
22+
Original: []byte{0xf6, 0x4, 0x0, 0x6, 0x3, 0x3},
23+
},
24+
}, {
25+
in: "502f3fff0f0f0a0d0f0d0d0f0f0f2f3e3f04",
26+
want: &PhevMessage{
27+
Type: 0x6f,
28+
Length: 0x12,
29+
Register: 0xc0,
30+
Data: []byte{0x30, 0x30, 0x35, 0x32, 0x30, 0x32, 0x32, 0x30, 0x30, 0x30, 0x10, 0x1, 0x0},
31+
Checksum: 0x3b,
32+
Xor: 0x3f,
33+
Original: []byte{0x6f, 0x10, 0x0, 0xc0, 0x30, 0x30, 0x35, 0x32, 0x30, 0x32, 0x32, 0x30, 0x30, 0x30, 0x10, 0x1, 0x0, 0x3b},
34+
},
35+
}, {
36+
in: "caa2a5a7a5a5a5a5dd",
37+
want: &PhevMessage{
38+
Type: 0x6f,
39+
Length: 0x9,
40+
Register: 0x2,
41+
Data: []byte{0x0, 0x0, 0x0, 0x0},
42+
Checksum: 0x78,
43+
Xor: 0xa5,
44+
Original: []byte{0x6f, 0x7, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x78},
45+
},
46+
}, {
47+
in: "3cf4f13360d4",
48+
want: &PhevMessage{
49+
Type: 0xcc,
50+
Length: 0x6,
51+
Register: 0xc3,
52+
Data: []byte{0x90},
53+
Checksum: 0x24,
54+
Xor: 0xf0,
55+
Ack: Ack,
56+
Original: []byte{0xcc, 0x4, 0x1, 0xc3, 0x90, 0x24},
57+
},
58+
}, {
59+
in: "4bf4f1c190a1",
60+
want: &PhevMessage{
61+
Type: 0xbb,
62+
Length: 0x6,
63+
Register: 0x31,
64+
Data: []byte{0x60},
65+
Checksum: 0x51,
66+
Xor: 0xf0,
67+
Ack: Ack,
68+
Original: []byte{0xbb, 0x4, 0x1, 0x31, 0x60, 0x51},
69+
},
70+
}, {
71+
in: "9ff6f0f3f1e59301",
72+
want: &PhevMessage{
73+
Type: 0x6f,
74+
Length: 0x8,
75+
Register: 0x3,
76+
Data: []byte{0x1, 0x15, 0x63},
77+
Checksum: 0xf1,
78+
Xor: 0xf0,
79+
Original: []byte{0x6f, 0x6, 0x0, 0x3, 0x1, 0x15, 0x63, 0xf1},
80+
},
81+
},
82+
}
83+
84+
for _, test := range tests {
85+
t.Run(test.in, func(t *testing.T) {
86+
data, err := hex.DecodeString(test.in)
87+
if err != nil {
88+
t.Fatal(err)
89+
}
90+
p := &PhevMessage{}
91+
if err := p.DecodeFromBytes(data); err != nil {
92+
t.Fatalf("DecodeFromBytes() unexpected error: %v", err)
93+
}
94+
p.Reg = nil // Skip reg test for now.
95+
if diff, eq := messagediff.PrettyDiff(test.want, p); !eq {
96+
t.Fatalf("DecodeFromBytes() diff=%s", diff)
97+
}
98+
99+
outData := test.want.EncodeToBytes()
100+
gotData := hex.EncodeToString(outData)
101+
if gotData != test.in {
102+
t.Fatalf("EncodeToBytes: Unexpected. got=%s want=%s", gotData, test.in)
103+
}
104+
})
105+
}
106+
}

‎protocol/raw.go

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package protocol
2+
3+
import (
4+
"encoding/hex"
5+
"fmt"
6+
)
7+
8+
func XorMessageWith(message []byte, xor byte) []byte {
9+
msg := make([]byte, len(message))
10+
for i := range message {
11+
msg[i] = message[i] ^ xor
12+
}
13+
return msg
14+
}
15+
16+
func Checksum(message []byte) byte {
17+
length := message[1] + 2
18+
19+
b := byte(0)
20+
for i := byte(0); ; i++ {
21+
if i >= length-1 {
22+
break
23+
}
24+
b = (byte)(message[i] + b)
25+
}
26+
return b
27+
}
28+
29+
func ValidateChecksum(message []byte) bool {
30+
length := message[1] + 2
31+
if len(message) < int(length) {
32+
return false
33+
}
34+
wantSum := message[length-1]
35+
36+
return Checksum(message) == wantSum
37+
}
38+
39+
// Validate and decode message. Returns the decoded/validated message,
40+
// plus any trailing data.
41+
func ValidateAndDecodeMessage(message []byte) ([]byte, byte, []byte) {
42+
if len(message) < 4 {
43+
fmt.Printf("Short msg\n")
44+
return nil, 0, nil
45+
}
46+
xor := message[2]
47+
msg := XorMessageWith(message, xor)
48+
if !ValidateChecksum(msg) {
49+
xor ^= 1
50+
msg = XorMessageWith(message, xor)
51+
if !ValidateChecksum(msg) {
52+
fmt.Printf("Bad sum for (%s)\n", hex.EncodeToString(message))
53+
return nil, 0, nil
54+
}
55+
}
56+
length := msg[1] + 2
57+
if len(message) > int(length) {
58+
return msg[:length], xor, message[length:]
59+
}
60+
return msg[:length], xor, nil
61+
}
62+
63+
func GetDecodedMessages(message []byte) [][]byte {
64+
msgs := [][]byte{}
65+
for {
66+
dec, _, rem := ValidateAndDecodeMessage(message)
67+
if dec != nil {
68+
msgs = append(msgs, dec)
69+
}
70+
if rem == nil {
71+
break
72+
}
73+
message = rem
74+
}
75+
return msgs
76+
}

‎protocol/raw_test.go

+178
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package protocol
2+
3+
import (
4+
"encoding/hex"
5+
"fmt"
6+
"strings"
7+
"testing"
8+
)
9+
10+
func hexCmp(got []byte, want string) string {
11+
gotS := hex.EncodeToString(got)
12+
if gotS != want {
13+
return fmt.Sprintf("got=%s want=%s", gotS, want)
14+
}
15+
return ""
16+
}
17+
18+
func TestXorMessage(t *testing.T) {
19+
tests := []struct {
20+
in, want string
21+
xor byte
22+
}{
23+
{
24+
in: "d8b2b7a9b7b725",
25+
xor: 0xb7,
26+
want: "6f05001e000092",
27+
},
28+
}
29+
30+
for _, test := range tests {
31+
t.Run(test.in, func(t *testing.T) {
32+
in, err := hex.DecodeString(test.in)
33+
if err != nil {
34+
t.Fatal(err)
35+
}
36+
got := XorMessageWith(in, test.xor)
37+
if diff := hexCmp(got, test.want); diff != "" {
38+
t.Errorf(diff)
39+
}
40+
})
41+
}
42+
}
43+
44+
func TestValidateChecksum(t *testing.T) {
45+
tests := []struct {
46+
in string
47+
want bool
48+
}{
49+
{
50+
in: "bb04016cf01c",
51+
want: true,
52+
}, {
53+
in: "f60400060303",
54+
want: true,
55+
}, {
56+
in: "f60400060304",
57+
want: false,
58+
},
59+
}
60+
61+
for _, test := range tests {
62+
t.Run(test.in, func(t *testing.T) {
63+
in, err := hex.DecodeString(test.in)
64+
if err != nil {
65+
t.Fatal(err)
66+
}
67+
if got, want := ValidateChecksum(in), test.want; got != want {
68+
t.Fatalf("got=%v want=%v", got, want)
69+
}
70+
})
71+
}
72+
}
73+
func TestValidateAndDecodeMessage(t *testing.T) {
74+
tests := []struct {
75+
in, want, remaining string
76+
xor byte
77+
}{
78+
{
79+
in: "06f4f0f6f3f306f4f0f6f3f3",
80+
want: "f60400060303",
81+
remaining: "06f4f0f6f3f3",
82+
xor: 0xf0,
83+
}, {
84+
in: "ff879094eda82091132d9091ece0a891906f6f93906f6f93c8",
85+
want: "6f1700047d38b00183bd00017c70380100ffff0300ffff0358",
86+
remaining: "",
87+
xor: 0x90,
88+
}, {
89+
in: "d8b2b7a9b7b725",
90+
want: "6f05001e000092",
91+
remaining: "",
92+
xor: 0xb7,
93+
}, {
94+
in: "4ab8bd95bc98",
95+
want: "f60401290024",
96+
remaining: "",
97+
xor: 0xbc,
98+
}, {
99+
in: "f20a000100000000000000fd",
100+
want: "f20a000100000000000000fd",
101+
remaining: "",
102+
xor: 0x00,
103+
}, {
104+
in: "502f3fff0f0f0a0d0f0d0d0f0f0f2f3e3f04",
105+
want: "6f1000c0303035323032323030301001003b",
106+
remaining: "",
107+
xor: 0x3f,
108+
}, {
109+
in: "3cf4f16e55e4",
110+
want: "cc04019ea514",
111+
remaining: "",
112+
xor: 0xf0,
113+
}, {
114+
in: "06f4f0f6f3f3",
115+
want: "f60400060303",
116+
remaining: "",
117+
xor: 0xf0,
118+
}, {
119+
in: "4bf4f19c00ec",
120+
want: "bb04016cf01c",
121+
remaining: "",
122+
xor: 0xf0,
123+
},
124+
}
125+
126+
for _, test := range tests {
127+
t.Run(test.in, func(t *testing.T) {
128+
in, err := hex.DecodeString(test.in)
129+
if err != nil {
130+
t.Fatal(err)
131+
}
132+
got, xor, gotRem := ValidateAndDecodeMessage(in)
133+
if gs := hex.EncodeToString(got); gs != test.want {
134+
t.Errorf("got=%s want=%s", gs, test.want)
135+
}
136+
if gs := hex.EncodeToString(gotRem); gs != test.remaining {
137+
t.Errorf("gotRem=%s want=%s", gs, test.remaining)
138+
}
139+
if xor != test.xor {
140+
t.Errorf("Xor got=%x want=%x", xor, test.xor)
141+
}
142+
})
143+
}
144+
}
145+
func TestDecodeMessages(t *testing.T) {
146+
tests := []struct {
147+
in, want string
148+
}{
149+
{
150+
in: "caa2a5a7a5a5a5a5dd4bf4f1f15596",
151+
want: "6f0700020000000078,bb040101a566",
152+
}, {
153+
in: "06f4f0f6f3f306f4f0f6f3f3",
154+
want: "f60400060303,f60400060303",
155+
}, {
156+
in: "ff879094eda82091132d9091ece0a891906f6f93906f6f93c8",
157+
want: "6f1700047d38b00183bd00017c70380100ffff0300ffff0358",
158+
},
159+
}
160+
161+
for _, test := range tests {
162+
t.Run(test.in, func(t *testing.T) {
163+
in, err := hex.DecodeString(test.in)
164+
if err != nil {
165+
t.Fatal(err)
166+
}
167+
got := GetDecodedMessages(in)
168+
gotList := []string{}
169+
for _, m := range got {
170+
gotList = append(gotList, hex.EncodeToString(m))
171+
}
172+
gotStr := strings.Join(gotList, ",")
173+
if gotStr != test.want {
174+
t.Errorf("got=%s want=%s", gotStr, test.want)
175+
}
176+
})
177+
}
178+
}

0 commit comments

Comments
 (0)
Please sign in to comment.