Multi device sync/backup
This commit is contained in:
commit
c9544a6421
0
.gitignore
vendored
Normal file
0
.gitignore
vendored
Normal file
8
.idea/.gitignore
vendored
Normal file
8
.idea/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
9
.idea/homecontroller.iml
Normal file
9
.idea/homecontroller.iml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/homecontroller.iml" filepath="$PROJECT_DIR$/.idea/homecontroller.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
6
.idea/vcs.xml
Normal file
6
.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
125
device.go
Normal file
125
device.go
Normal file
@ -0,0 +1,125 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
)
|
||||
|
||||
func getNewID(clients []StoredClient) uint32 {
|
||||
newID := uint32(0)
|
||||
for _, client := range clients {
|
||||
if client.ID >= newID {
|
||||
newID = client.ID + 1
|
||||
}
|
||||
}
|
||||
return newID
|
||||
}
|
||||
|
||||
func addDevice(name string, deviceType uint8, clients []StoredClient) (key []byte, usedID uint32) {
|
||||
key = make([]byte, 64)
|
||||
_, err := rand.Read(key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
usedID = getNewID(clients)
|
||||
storedClients = append(storedClients, StoredClient{
|
||||
Key: key,
|
||||
Name: name,
|
||||
ID: usedID,
|
||||
Type: deviceType,
|
||||
Privileges: 0,
|
||||
})
|
||||
return key, usedID
|
||||
}
|
||||
|
||||
func addController(name string, clients []StoredClient) (key []byte, usedID uint32) {
|
||||
key = make([]byte, 64)
|
||||
_, err := rand.Read(key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
usedID = getNewID(clients)
|
||||
storedClients = append(storedClients, StoredClient{
|
||||
Key: key,
|
||||
Name: name,
|
||||
ID: usedID,
|
||||
Type: 0,
|
||||
Privileges: 1,
|
||||
})
|
||||
return key, usedID
|
||||
}
|
||||
|
||||
func (client *Device) setChannel(id uint8, value uint8) error {
|
||||
if client.channels == nil || uint8(len(client.channels)) >= id {
|
||||
return errors.New("channel impossible to set")
|
||||
}
|
||||
var outBuffer []byte
|
||||
outBuffer = append(outBuffer, id)
|
||||
outBuffer = append(outBuffer, value)
|
||||
|
||||
return client.client.sendPacket(2, outBuffer)
|
||||
}
|
||||
|
||||
func (client *Device) getDeviceChannelResponse(targetDeviceID uint32, id uint8) error {
|
||||
if clients == nil || uint32(len(clients)) >= targetDeviceID {
|
||||
return errors.New("target device is nonexistent")
|
||||
}
|
||||
clientDevice := clients[targetDeviceID]
|
||||
if clientDevice.channels == nil || uint8(len(clientDevice.channels)) >= id {
|
||||
return errors.New("channel impossible to get")
|
||||
}
|
||||
var outBuffer []byte
|
||||
|
||||
var deviceID []byte
|
||||
binary.LittleEndian.PutUint32(deviceID, targetDeviceID)
|
||||
outBuffer = append(outBuffer, deviceID...)
|
||||
outBuffer = append(outBuffer, id)
|
||||
outBuffer = append(outBuffer, clientDevice.channels[id])
|
||||
|
||||
return client.client.sendPacket(4, outBuffer)
|
||||
}
|
||||
|
||||
func (client *Device) readChannel(id uint8) error {
|
||||
if client.channels == nil || uint8(len(client.channels)) >= id {
|
||||
return errors.New("channel impossible to get")
|
||||
}
|
||||
var outBuffer []byte
|
||||
outBuffer = append(outBuffer, id)
|
||||
|
||||
return client.client.sendPacket(3, outBuffer)
|
||||
}
|
||||
|
||||
func (client *Device) readChannels() error {
|
||||
if client.channels == nil || uint8(len(client.channels)) == 0 {
|
||||
return errors.New("channels impossible to get")
|
||||
}
|
||||
var err error
|
||||
for channelID := range client.channels {
|
||||
err = client.readChannel(uint8(channelID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (client *Device) receiveChannel(data []byte) error {
|
||||
if len(data) < 1 {
|
||||
return errors.New("channel data too short")
|
||||
}
|
||||
id := data[0]
|
||||
if client.channels == nil || uint8(len(client.channels)) >= id {
|
||||
return errors.New("channel impossible to get")
|
||||
}
|
||||
value := data[4:5][0]
|
||||
client.channels[id] = value
|
||||
return nil
|
||||
}
|
||||
|
||||
var deviceChannelCount = []uint8{0, 1, 1, 2}
|
||||
|
||||
//0: phone
|
||||
//1: switch
|
||||
//2: dimmer
|
||||
//3: gate
|
44
dimmerDevice.go
Normal file
44
dimmerDevice.go
Normal file
@ -0,0 +1,44 @@
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
type DimmerDevice struct {
|
||||
baseDevice *Device
|
||||
lastBrightness uint8
|
||||
}
|
||||
|
||||
// init
|
||||
func createDimmerDevice(device *Device) *DimmerDevice {
|
||||
device.typeID = 1
|
||||
return &DimmerDevice{baseDevice: device}
|
||||
}
|
||||
|
||||
func (dimmerDevice *DimmerDevice) on() error {
|
||||
if dimmerDevice.baseDevice.channels[0] != 1 {
|
||||
if dimmerDevice.lastBrightness == 0 {
|
||||
dimmerDevice.lastBrightness = 255
|
||||
}
|
||||
return dimmerDevice.baseDevice.setChannel(0, dimmerDevice.lastBrightness)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (dimmerDevice *DimmerDevice) off() error {
|
||||
if dimmerDevice.baseDevice.channels[0] != 2 {
|
||||
dimmerDevice.lastBrightness = dimmerDevice.baseDevice.channels[0]
|
||||
return dimmerDevice.baseDevice.setChannel(0, 1)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (dimmerDevice *DimmerDevice) getStringState() string {
|
||||
if dimmerDevice.baseDevice.channels[0] == 1 {
|
||||
return "OFF"
|
||||
} else if dimmerDevice.baseDevice.channels[0] > 1 {
|
||||
percentVal := dimmerDevice.baseDevice.channels[0] / 255
|
||||
return fmt.Sprintf("ON at %d%%", percentVal)
|
||||
}
|
||||
return "Unknown"
|
||||
}
|
48
gateDevice.go
Normal file
48
gateDevice.go
Normal file
@ -0,0 +1,48 @@
|
||||
package main
|
||||
|
||||
type GateDevice struct {
|
||||
baseDevice *Device
|
||||
}
|
||||
|
||||
// init
|
||||
func createGateDevice(device *Device) *GateDevice {
|
||||
device.typeID = 3
|
||||
return &GateDevice{baseDevice: device}
|
||||
}
|
||||
|
||||
//states:
|
||||
//0: unknown
|
||||
//1: closed
|
||||
//2: closing
|
||||
//3: opening
|
||||
//4: open
|
||||
|
||||
func (gateDevice *GateDevice) open() error {
|
||||
if gateDevice.baseDevice.channels[0] != 3 && gateDevice.baseDevice.channels[1] != 4 {
|
||||
return gateDevice.baseDevice.setChannel(0, 3)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (gateDevice *GateDevice) close() error {
|
||||
if gateDevice.baseDevice.channels[0] != 1 && gateDevice.baseDevice.channels[1] != 2 {
|
||||
return gateDevice.baseDevice.setChannel(0, 2)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (gateDevice *GateDevice) getStringState() string {
|
||||
switch gateDevice.baseDevice.channels[0] {
|
||||
case 1:
|
||||
return "Closed"
|
||||
case 2:
|
||||
return "Closing"
|
||||
case 3:
|
||||
return "Opening"
|
||||
case 4:
|
||||
return "Open"
|
||||
}
|
||||
return "Unknown"
|
||||
}
|
3
go.mod
Normal file
3
go.mod
Normal file
@ -0,0 +1,3 @@
|
||||
module homecontroller
|
||||
|
||||
go 1.23.0
|
0
go.sum
Normal file
0
go.sum
Normal file
54
main.go
Normal file
54
main.go
Normal file
@ -0,0 +1,54 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
)
|
||||
|
||||
var clients []*Device
|
||||
var storedClients []StoredClient
|
||||
|
||||
func main() {
|
||||
clients = make([]*Device, 0)
|
||||
data, err := os.ReadFile("clients.json")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = json.Unmarshal(data, &storedClients)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Create a connection to the server
|
||||
addr, err := net.ResolveTCPAddr("tcp", "0.0.0.0")
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("Error resolving address " + err.Error())
|
||||
}
|
||||
listen, err := net.ListenTCP("tcp", addr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// close listener
|
||||
defer func(listen *net.TCPListener) {
|
||||
_ = listen.Close()
|
||||
}(listen)
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listen.AcceptTCP()
|
||||
if err != nil {
|
||||
var opErr *net.OpError
|
||||
if errors.As(err, &opErr) && opErr.Err.Error() == "use of closed network connection" {
|
||||
log.Println("Listener closed, stopping server.")
|
||||
return
|
||||
}
|
||||
log.Println("Error accepting connection:", err)
|
||||
continue
|
||||
}
|
||||
go handleRequest(conn)
|
||||
}
|
||||
}()
|
||||
}
|
231
netCode.go
Normal file
231
netCode.go
Normal file
@ -0,0 +1,231 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Device struct {
|
||||
cipher *cipher.Block
|
||||
conn *net.TCPConn
|
||||
device *Device
|
||||
name string
|
||||
id uint32
|
||||
typeID uint8
|
||||
channels []uint8
|
||||
client *Device
|
||||
lastHeardFrom int64
|
||||
privileges uint8
|
||||
}
|
||||
|
||||
type StoredClient struct {
|
||||
Key []byte `json:"key"`
|
||||
Name string `json:"name"`
|
||||
ID uint32 `json:"id"`
|
||||
Type uint8 `json:"type"`
|
||||
Privileges uint8 `json:"privileges"`
|
||||
}
|
||||
|
||||
// sendPacket sends a packet with the given packetID and data over the provided TCP connection.
|
||||
func sendPacket(packetID uint8, data []byte, client *Device) error {
|
||||
// Create a buffer with the packet ID and data
|
||||
tempBuffer := append([]byte{packetID}, data...)
|
||||
|
||||
// Calculate the checksum of the packet ID + data
|
||||
checksum := crc32.ChecksumIEEE(tempBuffer)
|
||||
|
||||
// Convert the checksum to a 4-byte slice
|
||||
checksumBytes := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(checksumBytes, checksum)
|
||||
|
||||
var encryptedTempBuffer []byte
|
||||
|
||||
if client.cipher != nil {
|
||||
(*client.cipher).Encrypt(encryptedTempBuffer, tempBuffer)
|
||||
} else {
|
||||
encryptedTempBuffer = tempBuffer
|
||||
}
|
||||
// Calculate the total length (checksum + packet ID + data)
|
||||
packetLength := uint32(len(encryptedTempBuffer) + 4)
|
||||
|
||||
// Prepare the output buffer with the length field
|
||||
outBuffer := make([]byte, 4)
|
||||
|
||||
binary.LittleEndian.PutUint32(outBuffer, packetLength)
|
||||
// Append the tempBuffer (checksum + packet ID + data) to outBuffer
|
||||
outBuffer = append(outBuffer, checksumBytes...)
|
||||
outBuffer = append(outBuffer, tempBuffer...)
|
||||
|
||||
// Send the packet over the connection
|
||||
writeLen, err := client.conn.Write(outBuffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if writeLen != len(outBuffer) {
|
||||
return errors.New("write length does not match output buffer length")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (client *Device) sendPacket(packetID uint8, data []byte) error {
|
||||
return sendPacket(packetID, data, client)
|
||||
}
|
||||
|
||||
func (client *Device) receivePacket() (uint8, uint32, []byte, error) {
|
||||
return receivePacket(client)
|
||||
}
|
||||
|
||||
func updateLastSeen(client *Device) {
|
||||
if client.conn != nil {
|
||||
client.lastHeardFrom = time.Now().Unix()
|
||||
}
|
||||
}
|
||||
|
||||
// receivePacket receives a packet from the provided TCP connection, decrypts it, and verifies it.
|
||||
func receivePacket(client *Device) (uint8, uint32, []byte, error) {
|
||||
// Step 1: Read the packet length (4 bytes)
|
||||
lengthBuffer := make([]byte, 4)
|
||||
n, err := client.conn.Read(lengthBuffer)
|
||||
if err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
if n != len(lengthBuffer) {
|
||||
return 0, 0, nil, errors.New("read length does not match length buffer")
|
||||
}
|
||||
|
||||
packetLength := binary.LittleEndian.Uint32(lengthBuffer)
|
||||
|
||||
// Step 2: Allocate a buffer to hold the rest of the packet (checksum + packetID + data)
|
||||
packetBuffer := make([]byte, packetLength)
|
||||
n, err = client.conn.Read(packetBuffer)
|
||||
if err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
if n != int(packetLength) {
|
||||
return 0, 0, nil, errors.New("read length does not match packet length")
|
||||
}
|
||||
|
||||
// Step 3: Decrypt the packet buffer
|
||||
decryptedPacketBuffer := make([]byte, len(packetBuffer))
|
||||
if client.cipher != nil {
|
||||
(*client.cipher).Decrypt(decryptedPacketBuffer, packetBuffer)
|
||||
} else {
|
||||
decryptedPacketBuffer = packetBuffer
|
||||
}
|
||||
|
||||
// Step 4: Extract the checksum (4 bytes)
|
||||
receivedChecksum := binary.LittleEndian.Uint32(decryptedPacketBuffer[:4])
|
||||
|
||||
// Step 5: Extract the packet ID (1 byte)
|
||||
packetID := decryptedPacketBuffer[4]
|
||||
|
||||
// Step 6: Extract the data
|
||||
data := decryptedPacketBuffer[5:]
|
||||
|
||||
// Step 7: Verify the checksum
|
||||
calculatedChecksum := crc32.ChecksumIEEE(append([]byte{packetID}, data...))
|
||||
if receivedChecksum != calculatedChecksum {
|
||||
return 0, 0, nil, errors.New("checksum mismatch")
|
||||
}
|
||||
|
||||
// Return the packet ID and data
|
||||
updateLastSeen(client)
|
||||
return packetID, uint32(len(data)), data, nil
|
||||
}
|
||||
|
||||
func (client *Device) receiveConnectionRequest(data []byte) {
|
||||
if len(data) < 4 {
|
||||
clientID := binary.LittleEndian.Uint32(data[0:4])
|
||||
for _, clientLoop := range storedClients {
|
||||
if clientLoop.ID == clientID {
|
||||
client.name = clientLoop.Name
|
||||
client.id = clientID
|
||||
if clientLoop.Privileges > 0 {
|
||||
clientLoop.Type = 0
|
||||
}
|
||||
client.device = &Device{
|
||||
typeID: clientLoop.Type,
|
||||
channels: make([]uint8, deviceChannelCount[clientLoop.Type]),
|
||||
client: client,
|
||||
privileges: clientLoop.Privileges,
|
||||
}
|
||||
clientCipher, err := aes.NewCipher(clientLoop.Key)
|
||||
if err != nil {
|
||||
fmt.Println("Error creating AES cipher")
|
||||
return
|
||||
}
|
||||
client.cipher = &clientCipher
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (client *Device) receiveChannelUpdate(data []byte) {
|
||||
if len(data) < 2 && len(client.channels) != 0 {
|
||||
channelID := data[0]
|
||||
channelValue := data[1]
|
||||
if channelID >= uint8(len(client.channels)) {
|
||||
return
|
||||
}
|
||||
client.channels[channelID] = channelValue
|
||||
}
|
||||
}
|
||||
|
||||
func handleRequest(conn *net.TCPConn) {
|
||||
//serverbound packets:
|
||||
//0: start connection
|
||||
//1: reserved
|
||||
//2: channel value
|
||||
//3: list devices
|
||||
//4: set device channel
|
||||
//5: get device channel
|
||||
//6: list devices
|
||||
//7: list admins
|
||||
//8: add device
|
||||
//9: add controller
|
||||
|
||||
//clientbound packets:
|
||||
//2: set channel
|
||||
//3: get channel
|
||||
//4: channel from device
|
||||
//5: added device key and id
|
||||
//6: device entry
|
||||
//7: admin entry
|
||||
|
||||
client := Device{
|
||||
cipher: nil,
|
||||
conn: conn,
|
||||
}
|
||||
clients = append(clients, &client)
|
||||
defer func(conn *net.TCPConn) {
|
||||
_ = conn.Close()
|
||||
}(conn)
|
||||
for {
|
||||
packetID, _, packetData, err := receivePacket(&client)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch packetID {
|
||||
case 0:
|
||||
if client.cipher == nil {
|
||||
client.receiveConnectionRequest(packetData)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
case 1:
|
||||
|
||||
case 2:
|
||||
if client.cipher != nil {
|
||||
client.receiveChannelUpdate(packetData)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
42
switchDevice.go
Normal file
42
switchDevice.go
Normal file
@ -0,0 +1,42 @@
|
||||
package main
|
||||
|
||||
type SwitchDevice struct {
|
||||
baseDevice *Device
|
||||
}
|
||||
|
||||
// init
|
||||
func createSwitchDevice(device *Device) *SwitchDevice {
|
||||
device.typeID = 1
|
||||
return &SwitchDevice{baseDevice: device}
|
||||
}
|
||||
|
||||
//states:
|
||||
//0: unknown
|
||||
//1: on
|
||||
//2: off
|
||||
|
||||
func (switchDevice *SwitchDevice) on() error {
|
||||
if switchDevice.baseDevice.channels[0] != 1 {
|
||||
return switchDevice.baseDevice.setChannel(0, 1)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (switchDevice *SwitchDevice) off() error {
|
||||
if switchDevice.baseDevice.channels[0] != 2 {
|
||||
return switchDevice.baseDevice.setChannel(0, 2)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (switchDevice *SwitchDevice) getStringState() string {
|
||||
switch switchDevice.baseDevice.channels[0] {
|
||||
case 1:
|
||||
return "On"
|
||||
case 2:
|
||||
return "Off"
|
||||
}
|
||||
return "Unknown"
|
||||
}
|
Loading…
Reference in New Issue
Block a user