📜 ⬆️ ⬇️

Access Controller for Go + Raspberry Pi + Arduino Nano

I want to share the next solution of the trivial task of implementing a network access controller (ACS).

The background to the emergence of this task is, as is often the case, the desire of the customer to get a special functionality of the ACS controller. This special functionality is as follows:


In the hard blocking mode, the lock cannot be opened either with a key or through a button or via HTTP. Disconnection occurs either by canceling the blocking mode or by resetting the controller by power.

Considering that the customer was ready to pay, practically, any money, it was not planned to initially develop his controller, but it was decided to find a controller ready on the market and implement the task. But in practice everything turned out to be not so joyful. As a clarification, it is necessary to say that the customer’s office has implemented automation (the Smart Home system) based on KNX + Control4.
')
The available network controllers are very functional, but in most cases this functionality is wired by the manufacturer’s firmware and naturally cannot be changed. For example, none of the controllers we reviewed (I will not give a list so that it does not look like an advertisement or anti-advertising, and in general for the meaning of the article, it doesn’t matter) does not have the function of closing the door by a button (surely some controllers can maybe we missed them). But the network controller can theoretically receive various commands via HTTP, including open / close, and Control4 can send these commands. But the SDK controllers available for the controllers in question are implemented on .NET libraries of older versions 1,2,3, which implied using Windows PCs as a gateway (now tomatoes from indignation of .NET developers should fly into me, such as the .NetCore, Mono and etc.). Surely, it was possible for .Net to adapt the development to Linux, but there was no confidence in the correctness and stability of this approach at that time.

Another problem was the key lock. Only one controller handled this task easily, a budgetary one (let me designate one of the applicants) Z-5R from Ironlogic. It has a “Trigger” mode, but the button is only for opening. Sane information on the SDK from technical support has not received. In general, after analyzing and evaluating all the information, it was decided to develop our own decision.

As a hardware platform, we decided to use a bunch of Raspberry Pi (rev.B) + Arduino Nano. Arduino works fine with low-level interfaces, and on the Malinka you can raise a full-fledged network stack and use high-level programming languages. Communication between the boards via USB (via Serial Port)

image
This diagram does not indicate the sound indication element (sound speaker), the implementation of which is reflected in the code for the Arduino. From the code it will be seen that it is connected to pin - 9.

The following components were used for implementation:

• Raspberry Pi (rev. B) - 1 pc.
• Arduino Nano - 1 pc.
• Sound speaker - 1 pc.
• Touch Memory Key Reader (iButton) - 1 pc.
• Resistor 220 Ohm - 1 tsh.
• Relay 12V - 1 pc.
• Electromagnetic lock 12V - 1 pc.

Requirements for the development environment

Before you develop on Go, you need to prepare the environment (since I was developing in Windows, the list of dependencies is described for this OS). I will not elaborate on each item. much has been said and written about them.

  1. Installing Go under Windows
  2. Install development tools. I used Visual Studio Code . Very convenient and functional code editor. Recommend! Although you can use JetBrains IDE Goland for Go
  3. Configure Visual Studio Code to work with Go . The instruction is in English, but everything is described quite clearly.
  4. Installed Arduino IDE - to fill the sketch.
  5. Git repository tool (to download Go packages from Github)

Sketch access control for Arduino

The code for the Arduino is very simple and straightforward. The only point to which you need to pay attention is that the OneWire library is not included in the standard set and must be downloaded .

A small feature in the code is to save the current state of the lock in the EEPROM of the microcontroller, in case, in order to remember the current state of the lock if there is a short failure and power loss.

Sketch access control for Arduino
#include <OneWire.h> #include <EEPROM.h> #define RELAY1 6 //    boolean isClose; //     boolean hl=false; //      byte i; OneWire ds(7); //   byte addr[8]; //   String inCommand = ""; //    Raspberry Pi char character; //    void setup() { Serial.begin(9600); pinMode(RELAY1, OUTPUT); stateRead(); } void loop(){ if (ds.search(addr)) { ds.reset_search(); if ( OneWire::crc8( addr, 7) != addr[7]) { } else { if(!hl){ for( i = 0; i < 8; i++) { Serial.print(addr[i],HEX); } Serial.println(); } } } ds.reset(); delay(500); while(Serial.available()) { character = Serial.read(); inCommand.concat(character); } if (inCommand=="hlock1"){ hl=true; r_close(); Serial.println("HardLock Enable"); } if (inCommand=="hlock0"){ hl=false; Serial.println("HardLock Disable"); } if (inCommand != "" && !hl) { if ((inCommand=="open") && (isClose) ){ r_open(); } if ((inCommand=="close") &&(!isClose)){ r_close(); } } inCommand=""; } void r_open(){ digitalWrite(RELAY1,LOW); isClose=false; stateSave(isClose); SoundTone(0); delay(100); Serial.println("Relay Open "); } void r_close(){ digitalWrite(RELAY1,HIGH); isClose=true; stateSave(isClose); SoundTone(1); delay(100); Serial.println("Realy Close"); } void stateSave(boolean st) //      EEPROM { if (st) { int val=1; EEPROM.write(0,val); } else { int val=0; EEPROM.write(0,val); } } void stateRead() { int val; val= (EEPROM.read(0)); if (val==1) r_close(); else r_open(); } void SoundTone(boolean cmd){ if(!cmd){ for (int i=0;i<10;i++){ tone(9, 815, 100); delay(250); } } else { for (int i=0;i<4;i++){ tone(9, 395, 500); delay(350); } } noTone(9); } 


The main controller code, as already mentioned, is written in Go. Almost all libraries are taken from standard Go sources, with the exception of two.

The first is the main BoltDB database, of type key \ value. Working with her does not require "dancing with a tambourine", it is very simple and fast. The second implements work with the COM port.

The main algorithm of the controller is as follows:

  1. At startup, the configuration is read from the config.json file;
  2. A small HTTP REST service is started;
  3. Opens the COM port for data exchange with the Arduino;
  4. A channel of type bool is created which is sent along with a pointer to a COM port to a go-routine where reading ID keys from the Arduino takes place;
  5. Then a cycle starts in which the data is waiting from the channel sent earlier to the go-routine. Data will enter the channel only if the read key exists in the database and is active, after which the relay switching command will be sent to the Arduino.

Adding, deleting, reading keys and locking control is carried out through HTTP requests. Many will immediately say that it is stupid, because Anyone can make a request to the controller. Yes, I agree that security needs to be further refined, but as a preventive measure, the configuration file has the ability to change the names of endpoints for various commands. It is a little difficult to seize control of the controller by outsiders.

Controller code
 package main import ( "bufio" "encoding/json" "io/ioutil" "fmt" "log" "net/http" "os" "regexp" "time" "github.com/boltdb/bolt" "github.com/tarm/serial" ) const dbname = "access.db" //    var isOpen, isHLock bool = false, false var serialPort *serial.Port func main() { //   config, err := readConfig() if err != nil { fmt.Printf("Error read config file %s", err.Error()) return } //   f, err := os.OpenFile(config.LogFilePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { log.Fatalf("error opening file: %v", err) } defer f.Close() log.SetOutput(f) //    HTTP- http.HandleFunc("/"+config.NormalModeEndpoint, webNormalMode) http.HandleFunc("/"+config.HardLockModeEndpoint, webHLockMode) http.HandleFunc("/"+config.CloseEndpoint, webCloseRelay) http.HandleFunc("/"+config.OpenEndpoint, webOpenRelay) http.HandleFunc("/"+config.AddKeyEndpoint, addKey) http.HandleFunc("/"+config.ReadKeysEndpoint, readKeys) http.HandleFunc("/"+config.DeleteKeyEndpoint, deleteKey) go http.ListenAndServe(":"+config.HTTPPort, nil) log.Printf("Listening on port %s...", config.HTTPPort) //    db, err := bolt.Open(dbname, 0600, nil) if err != nil { log.Fatal(err) } db.Close() //   Serial  c := &serial.Config{Name: config.SerialPort, Baud: 9600} s, err := serial.OpenPort(c) if err != nil { fmt.Printf("Error open serial port %s ", err.Error()) log.Fatal(err) } serialPort = s //     , go- ch := make(chan bool) // wait chanel until key is valid go getData(ch, s) for { time.Sleep(time.Second) tmp := <-ch if tmp { if isOpen { closeRelay() } else { openRelay() } } } } func getData(ch chan bool, s *serial.Port) { for { reader := bufio.NewReader(s) reply, err := reader.ReadBytes('\n') if err != nil { log.Fatal(err) } k := string(reply) if chk := checkKey(k); chk { ch <- chk time.Sleep(2 * time.Second) } } } func invertBool() { //    isOpen = !isOpen } func checkErr(err error) { if err != nil { panic(err) } } func boltStore(value Key) { db, err := bolt.Open(dbname, 0600, nil) if err != nil { log.Fatal(err) } defer db.Close() db.Update(func(tx *bolt.Tx) error { b, err := tx.CreateBucketIfNotExists([]byte("keys")) if err != nil { return err } return b.Put([]byte(value.Key), []byte(value.isEnable)) }) } func boltRead(key string) bool { var strKey string db, err := bolt.Open(dbname, 0600, nil) if err != nil { log.Fatal(err) return false } defer db.Close() db.View(func(tx *bolt.Tx) error { re := regexp.MustCompile(`\r\n`) key := re.ReplaceAllString(key, "") re = regexp.MustCompile(`\n`) key = re.ReplaceAllString(key, "") re = regexp.MustCompile(`\r`) key = re.ReplaceAllString(key, "") log.Printf("Readed key: %s\n", key) b := tx.Bucket([]byte("keys")) v := b.Get([]byte(key)) strKey = string(v) return nil }) if strKey == "1" { log.Printf("Key %s valid\n", key) return true } return false } func addKey(w http.ResponseWriter, r *http.Request) { params := r.URL.Query() var key Key key.Key = params.Get("key") key.isEnable = params.Get("enable") boltStore(key) log.Printf("You add the key %s", key.Key) fmt.Fprintln(w, "You add the key", key.Key) } func readKeys(w http.ResponseWriter, r *http.Request) { keys := make(map[string]string) db, err := bolt.Open(dbname, 0600, nil) if err != nil { log.Fatal(err) } defer db.Close() db.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("keys")) b.ForEach(func(k, v []byte) error { keys[string(k)] = string(v) fmt.Printf("map: %s\n", keys[string(k)]) return nil }) return nil }) data, _ := json.Marshal(keys) fmt.Fprintln(w, string(data)) } func deleteKey(w http.ResponseWriter, r *http.Request) { params := r.URL.Query() deleteKey := params.Get("key") db, err := bolt.Open(dbname, 0600, nil) if err != nil { log.Fatal(err) } defer db.Close() db.Update(func(tx *bolt.Tx) error { // Retrieve the users bucket. // This should be created when the DB is first opened. b := tx.Bucket([]byte("keys")) err := b.Delete([]byte(deleteKey)) if err != nil { fmt.Printf("Key: \"%s\" delete failed: %s\n", deleteKey, err.Error()) return err } fmt.Fprintf(w, "Key: \"%s\" deleted succesfully\n", deleteKey) // Persist bytes to users bucket. return nil }) } func webNormalMode(w http.ResponseWriter, r *http.Request) { isHLock = false _, err := serialPort.Write([]byte("hlock0")) if err != nil { log.Fatal(err) } fmt.Fprintln(w, "Normal Mode") } func webHLockMode(w http.ResponseWriter, r *http.Request) { _, err := serialPort.Write([]byte("hlock1")) if err != nil { log.Fatal(err) } isHLock = true fmt.Fprintln(w, "HardLock Mode") } func webCloseRelay(w http.ResponseWriter, r *http.Request) { switchRelay() fmt.Fprintln(w, "switch relay") } func webOpenRelay(w http.ResponseWriter, r *http.Request) { openRelay() fmt.Fprintln(w, "open lock") } func closeRelay() { _, err := serialPort.Write([]byte("close")) if err != nil { log.Fatal(err) } invertBool() log.Println("Close") } func openRelay() { _, err := serialPort.Write([]byte("open")) if err != nil { log.Fatal(err) } invertBool() log.Println("Open") } func switchRelay() { if isOpen { closeRelay() } else { openRelay() } } func checkKey(key string) bool { if boltRead(key) { return true } return false } func readConfig() (*Config, error) { plan, _ := ioutil.ReadFile("config.json") config := Config{} err := json.Unmarshal([]byte(plan), &config) return &config, err } 


I built the binary file on the Raspberry Pi itself (of course, I had to install all the dependencies for Go on raspberries).

 GOOS=linux GOARCH=arm go build -o /home/pi/skud-go/skud-go 

Also, the main thing is not to forget to put the following dependent files together with the binary file:

 config.json access.db 

config.json
{
"SerialPort": "/ dev / ttyUSB0",
"HttpPort": "80"
"NormalModeEndpoint": "normal",
"HardLockModeEndpoint": "block",
"CloseEndpoint": "close",
"OpenEndpoint": "open",
"AddKeyEndpoint": "addkey",
"DeleteKeyEndpoint": "deletekey",
"ReadKeysEndpoint": "readkeys",
"LogFilePath": "/ var / log / skud-go.log"
}

Types of data controller. skud_type.go
 package main //Key     type Key struct { Key string isEnable string } //Config    type Config struct { SerialPort string `json:"serialPort"` HTTPPort string `json:"httpPort"` NormalModeEndpoint string `json:"normalModeEndpoint"` HardLockModeEndpoint string `json:"hardLockModeEndpoint"` CloseEndpoint string `json:"closeEndpoint"` OpenEndpoint string `json:"openEndpoint"` AddKeyEndpoint string `json:"addKeyEndpoint"` DeleteKeyEndpoint string `json:"deleteKeyEndpoint"` ReadKeysEndpoint string `json:"readKeysEndpoint"` LogFilePath string `json:"logFilePath"` } 


To start the controller as a service, you need to create an additional unit-file. Such a file tells the systemd initialization system how to manage this or that resource. Services are the most common type of unit-files, defining dependencies and parameters for starting and stopping a program.

Create such a file for skud-go. The file will be called skud-go.service and stored in / etc / systemd / system.

 sudo nano /etc/systemd/system/skud-go.service 

File contents:

 [Unit] Description=Access Control System Controller by Go After=network.target [Service] User=pi ExecStart=/home/pi/skud-go/skud-go [Install] WantedBy=multi-user.target 

To start a new service, enter:

 sudo systemctl start skud-go 

Now you need to enable the autostart of this service:

 sudo systemctl enable skud-go 

The result was a fairly simple and functional uptime controller of which, for more than 6 months (tiny of course, but still ahead). I hope this article will be useful for someone.

→ Sources are available on Github

Source: https://habr.com/ru/post/357964/


All Articles