Access Controller on Go + Raspberry Pi + Arduino Nano

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

The background to the occurrence of this task is, as often happens, in the desire of the customer to receive special functionality of the ACS controller. This particular functionality is as follows:

  • The controller must be networked (opening, closing the lock and the "hard lock" mode)
  • Opening and closing by button (trigger mode)
  • Opening and closing by key (trigger mode)
  • Hard Lock mode of input / output
  • Add, read and delete keys
  • Key lock
  • Event logging

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

Considering that the customer was ready to pay almost any money, it was not planned to initially develop his controller, but it was decided to find a controller ready in the market and realize the task. But in practice, everything turned out to be not so joyful. As a clarification, it must be said that the customer’s office has implemented automation (the Smart Home system) based on KNX + Control4.

The existing network controllers are very functional, but in most cases this functionality is protected by the manufacturer’s firmware and it is naturally impossible to change it. For example, none of the controllers that we examined (I will not give a list so that it does not look like advertising or anti-advertising, and in general for the meaning of the article, it doesn’t matter) has the function of closing the door by a button (for sure, some controllers can maybe we missed them). But the network controller can theoretically receive various HTTP commands, including opening / closing, and the Control4 controller can send these commands. But the SDKs available for the controllers under consideration were implemented on .NET libraries of old versions 1,2,3, which implied using a Windows PC as a gateway (now, tomatoes from the indignation of .NET developers should fly into me, like that is .NetCore, Mono, etc.). Surely, it was possible .Net developmentto adapt for Linux, but at that time there was no confidence in the correctness and stability of this approach.

Another problem was locking the key. Only one controller easily coped with this task, a budget (let me designate one of the applicants) Z-5R from Ironlogic. He has the “Trigger” mode, but the button is only for opening. I did not receive any sane information on the SDK from technical support. In general, having analyzed and evaluated all the information, it was decided to develop our own solution.

As a hardware platform, we decided to use a bunch of Raspberry Pi (rev.B) + Arduino Nano. Arduino works great with low-level and interfaces, and on the “raspberry” you can raise a full-fledged network stack and use high-level programming languages. Communication between the boards is 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. It will be seen from the code 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.
• 220 Ohm resistor - 1 t.
• 12V relay - 1 pc.
• Electromagnetic lock 12V - 1 pc.

Requirements for the development environment

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

  1. Install Go on Windows
  2. Install development tools. I used Visual Studio Code . Very convenient and functional code editor. Recommend! Although for Go you can use the JetBrains Goland IDE
  3. Setting up 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 (for downloading Go packages from Github)

Sketch ACS for Arduino

The code for Arduino is very simple and straightforward. The only thing you need to pay attention to 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 order to remember the current state of the lock if there is suddenly a short failure and loss of power.

Sketch SKUD for Arduino
#include 
#include 
#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, such as key \ value. Work with it does not require "dancing with a tambourine", it is very simple and fast. The second implements work with a COM port.

The main controller operation algorithm is as follows:

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

Adding, deleting, reading keys and managing the lock is done through HTTP requests. Many will immediately say that this is stupid, because anyone can fulfill the request to the controller. Yes, I agree that security needs to be further developed, but as a preventive measure, the configuration file has the ability to change the names of endpoints for various teams. It is a little difficult to capture the 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 assembled the binary file on the Raspberry Pi itself (naturally, 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 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 "
}

Controller data types. 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 a particular resource. Services - the most common type of unit-files that defines the dependencies and parameters of 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 autorun of this service:

sudo systemctl enable skud-go

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

→ Sources available on Github

Also popular now: