merle

package module
v0.0.52 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 26, 2022 License: BSD-3-Clause Imports: 32 Imported by: 2

README

Merle

Go Reference Go Report Card

Gopher Thing

Merle is a framework for building secure web-apps for your IoT project.

Put your Thing on the Internet with Merle.

Merle uses the Go programming language.

Installation

$ go get -u github.com/merliot/merle

Examples

Documentation

Project Web Page

  • Find Getting Started, Tutorial, Examples and useful Guides.

Code Reference

Hello, world!

Simple hello world app to blink the LED on a Raspberry Pi and serve up "Hello, World!". If you don't have a Raspberry Pi, you can still try the app.

Create a new Go project:

$ mkdir blink
$ cd blink
$ go mod init blink
$ go mod tidy

Create the file blink.go from:

package main

import (
        "time"

        "github.com/merliot/merle"
        "gobot.io/x/gobot/drivers/gpio"
        "gobot.io/x/gobot/platforms/raspi"
)

type blink struct {
}

func (b *blink) run(p *merle.Packet) {

        adaptor := raspi.NewAdaptor()
        adaptor.Connect()

        led := gpio.NewLedDriver(adaptor, "11")
        led.Start()

        for {
                led.Toggle()
                time.Sleep(time.Second)
        }
}

func (b *blink) Subscribers() merle.Subscribers {
        return merle.Subscribers{
                merle.CmdRun:  b.run,
        }
}

func (b *blink) Assets() *merle.ThingAssets {
        return &merle.ThingAssets{
		HtmlTemplateText: "Hello, world!\n",
	}
}

func main() {
        thing := merle.NewThing(&blink{})
        thing.Run()
}

Build it:

go build

Run it (in the background):

./blink &

The LED on the Raspberry Pi should be toggling on/off every second.

The app is a web server listening on port :8080. Let's use curl to hit the web server:

curl localhost:8080
Hello, world!

Need help?

Contributing

For contribution guidelines, please go to here.

License

Licensed under the BSD 3-Clause License

Copyright (c) 2021-2022 Scott Feldman (sfeldma@gmail.com).

TODO

My TODO list, not in any particular order. (Help would be much appreciated).

  • Thing-wide setting for a websocket ping/pong messages to close half-open sockets (send ping from Thing side of websocket)
  • Can we use something smaller like a container to run Thing Prime rather than a full VM?
  • favicon.ico support? just add a /favicon.ico file?
  • Investigate if Merle framework could be written in other languages (Rust?). Assests (js/html/etc) wouldn't need to change. Thing code would be rewritten in new language. A Thing written in one language should interoperate with another Thing written in another language?
  • More tests!
  • I'm not a Security expert. Need review of Merle by some who are.
  • I'm not a JavaScript/HTML expert. Need review of Merle by experts.

Documentation

Index

Constants

View Source
const (
	// CmdInit is guaranteed to be the first message a new Thing will see.
	// Thing can optionally subscribe and handle CmdInit via Subscribers(),
	// to initialize Thing's state.
	//
	// CmdInit is not sent to Thing Prime.  Thing Prime will get its
	// initial state with a GetState call to Thing.
	CmdInit = "_CmdInit"

	// CmdRun is Thing's main loop.  All Things must subscribe and handle
	// CmdRun, via Subscribers().  CmdRun should run forever; it is an error
	// for CmdRun handler to exit.
	//
	// CmdRun is not sent to Thing Prime.  Thing Prime does not have a main
	// loop.
	//
	// If the Thing is a bridge, CmdRun is also sent to the bridge bus on
	// startup of the bridge, via BridgeSubscribers().  In this case, CmdRun
	// is optional and doesn't need to run forever.
	CmdRun = "_CmdRun"

	// GetIdentity requests Thing's identity.  Thing does not need to
	// subscribe to GetIdentity.  Thing will internally respond with a
	// ReplyIdentity message.
	GetIdentity = "_GetIdentity"

	// Response to GetIdentity.  ReplyIdentity message is coded as
	// MsgIdentity.
	ReplyIdentity = "_ReplyIdentity"

	// GetState requests Thing's state.  Thing should respond with a
	// ReplyState message containing Thing's state.
	GetState = "_GetState"

	// Response to GetState.  ReplyState message coding is Thing-specific.
	//
	// It is convenient to use Thing's type struct (the Thinger) as the
	// container for Thing's state.  Just include a Msg member and export
	// any other state members (with an uppercase leading letter).  Then
	// the whole type struct can be passed in p.Marshal() to form the
	// response.
	//
	//	type thing struct {
	//		Msg       string
	//		StateVar0 int
	//		StateVar1 bool
	//		// non-exported members
	//	}
	//
	//	func (t *thing) init(p *merle.Packet) {
	//		t.StateVar0 = 42
	//		t.StateVar1 = true
	//	}
	//
	//	func (t *thing) getState(p *merle.Packet) {
	//		t.Msg = merle.ReplyState
	//		p.Marshal(t).Reply()
	//	}
	//
	// Will send JSON message:
	//
	//  {
	//	"Msg": "_ReplyState",
	//	"StateVar0": 42,
	//	"StateVar1": true,
	//  }
	ReplyState = "_ReplyState"

	// EventStatus message is an unsolicited notification that a child
	// Thing's connection status has changed.
	//
	// EventStatus message is coded as MsgEventStatus.
	EventStatus = "_EventStatus"
)

System messages. System messages are prefixed with '_'.

Variables

This section is empty.

Functions

func Broadcast

func Broadcast(p *Packet)

Subscriber helper function to broadcast Packet.

In this example, any Packets received with message Alert are broadcast to all other listeners:

return merle.Subscribers{
	...
	"Alert": merle.Broadcast,
}

func NoInit added in v0.0.27

func NoInit(p *Packet)

Subscriber helper function to do nothing on CmdInit. Example:

return merle.Subscribers{
	merle.CmdInit: merle.NoInit,
	...
}

func ReplyGetIdentity added in v0.0.24

func ReplyGetIdentity(p *Packet)

Subscriber helper function to GetIdentity. Example of chaining the EventStatus change notification to send a GetIdentity request:

return merle.Subscribers{
	...
	merle.EventStatus: merle.ReplyGetIdentity,
	merle.ReplyIdentity: t.identity,
}

func ReplyGetState added in v0.0.24

func ReplyGetState(p *Packet)

Subscriber helper function to GetState

func ReplyStateEmpty added in v0.0.24

func ReplyStateEmpty(p *Packet)

Subscriber helper function to return empty state in response to GetState. Example:

return merle.Subscribers{
	...
	merle.GetState: merle.ReplyStateEmpty,
}

func RunForever

func RunForever(p *Packet)

Subscriber helper function to run forever. Only applicable for CmdRun.

return merle.Subscribers{
	...
	merle.CmdRun: merle.RunForever,
}

Types

type BridgeThingers

type BridgeThingers map[string]func() Thinger

BridgeThingers is a map of functions which can generate Thingers, keyed by a regular expression (re) of the form: id:model:name. The keys specify which Things can attach to the bridge.

type Bridger

type Bridger interface {

	// Map of Thingers supported by Bridge.  Map keyed by a regular
	// expression (re) of the form: id:model:name specifying which Things
	// can attach to the bridge. E.g.:
	//
	//	return merle.BridgeThingers{
	//		".*:relays:.*": func() merle.Thinger { return relays.NewRelays() },
	//		".*:bmp180:.*": func() merle.Thinger { return bmp180.NewBmp180() },
	//	}
	//
	// In this example, a Thing with [id:model:name] = "01234:relays:foo"
	// would match the first entry.  Another Thing with "8888:foo:bar"
	// would not match either entry and would not attach.
	BridgeThingers() BridgeThingers

	// List of subscribers on Bridge bus.  All packets from all connected
	// Things (children) are forwarded to the Bridge bus and tested against
	// the BridgeSubscribers.
	BridgeSubscribers() Subscribers
}

A Thing implementing the Bridger interface is a Bridge

type Msg added in v0.0.24

type Msg struct {
	Msg string
}

All messages in Merle build on this basic struct. All messages have a member Msg which is the message type, a string that's unique within the Thing's message namespace.

System messages type Msg is prefixed with a "_". Regular Thing messages should not be prefixed with "_".

type MsgEventStatus added in v0.0.24

type MsgEventStatus struct {
	Msg    string
	Id     string
	Online bool
}

Event status change notification message. On child connect or disconnect, this notification is sent to:

1. If Thing Prime, send to all listeners (browsers) on Thing Prime.

2. If Bridge, send to mother bus and to bridge bus.

type MsgIdentity

type MsgIdentity struct {
	Msg         string
	Id          string
	Model       string
	Name        string
	Online      bool
	StartupTime time.Time
}

Thing identification message return in ReplyIdentity

type Packet

type Packet struct {
	// contains filtered or unexported fields
}

A Packet is the basic unit of communication in Merle. Thing Subscribers() receive, process and optional forward Packets. A Packet contains a single message and the message is JSON-encoded.

func (*Packet) Broadcast

func (p *Packet) Broadcast()

Broadcast the Packet to everyone else on the bus. Do not hold locks when calling Broadcast().

func (*Packet) Copy added in v0.0.48

func (p *Packet) Copy(dst *Packet)

Copy Packet's message to dst Packet

func (*Packet) From added in v0.0.48

func (p *Packet) From() string

func (*Packet) IsThing added in v0.0.24

func (p *Packet) IsThing() bool

Test if this is the real Thing or Thing Prime.

If p.IsThing() is not true, then we're on Thing Prime and should not access device I/O and only update Thing's software state. If p.IsThing() is true, then this is the real Thing and we can access device I/O.

func (*Packet) Marshal

func (p *Packet) Marshal(msg interface{}) *Packet

JSON-encode the message into the Packet

func (*Packet) Reply

func (p *Packet) Reply()

Reply back to sender of Packet. Do not hold locks when calling Reply().

func (*Packet) Send added in v0.0.24

func (p *Packet) Send(dst string)

Send Packet to destination TODO: Use restrictions? Only to be called from bridge, or could be called TODO: from child to talk to another child, over a bridge?

func (*Packet) Src added in v0.0.24

func (p *Packet) Src() string

Src is the Packet's originating Thing's Id. If the Packet originated internally, then Src() is "SYSTEM".

func (*Packet) String

func (p *Packet) String() string

String representation of Packet message

func (*Packet) Unmarshal

func (p *Packet) Unmarshal(msg interface{})

JSON-decode the message from the Packet

type Subscribers

type Subscribers map[string]func(*Packet)

Subscribers is a map of message subscribers, keyed by Msg type. On Packet receipt, the Packet Msg is used to lookup a subscriber. If a match, the subscriber handler is called to process the Packet.

Here's an example Subscribers() map:

func (t *thing) Subscribers() merle.Subscribers {
	return merle.Subscribers{
		merle.CmdInit:     t.init,
		merle.CmdRun:      t.run,
		merle.GetState:    t.getState,
		merle.EventStatus: nil,
		"SetPoint":        t.setPoint,
	}

A subscriber handler is a function that takes a Packet pointer as it's only argument. An example handler for the "SetPoint" Msg above:

func (t *thing) setPoint(p *merle.Packet) {
	// do something with Packet p
}

If the handler is nil, a Packet will be dropped silently.

If the key "default" exists, then the default handler is called for any non-matching Packets. Here's an example BridgeSuscribers() that silently drops all packets except CAN messages:

func (b *bridge) BridgeSubscribers() merle.Subscribers {
	return merle.Subscribers{
		"CAN":     merle.Broadcast, // broadcast CAN msgs to everyone
		"default": nil,             // drop everything else silently
	}
}

type Thing

type Thing struct {
	// Thing's configuration
	Cfg ThingConfig
	// contains filtered or unexported fields
}

Thing made from a Thinger.

func NewThing

func NewThing(thinger Thinger) *Thing

NewThing returns a Thing built from a Thinger.

type thing struct {
	// Implements Thinger interface
}

func main() {
	merle.NewThing(&thing{}).Run()
}

func (*Thing) Run

func (t *Thing) Run() error

Run Thing. An error is returned if Run() fails. Configure Thing before running.

func main() {
	thing := merle.NewThing(&thing{})
	thing.Cfg.PortPublic = 80  // run public web server on port :80
	log.Fatalln(thing.Run())
}

type ThingAssets

type ThingAssets struct {

	// Directory on file system for Thing's assets (html, css, js, etc)
	// This is an absolute or relative directory.  If relative, it's
	// relative to the Thing's binary path.
	AssetsDir string

	// Path to Thing's HTML template file, relative to AssetsDir.
	HtmlTemplate string

	// HtmlTemplateText is text passed in lieu of a template file.
	// HtmlTemplateText takes priority over HtmlTemplate, if both are
	// present.
	HtmlTemplateText string
}

type ThingConfig

type ThingConfig struct {

	// ########## Thing configuration.
	//
	// [Optional] Thing's Id.  Ids are unique within an application to
	// differentiate one Thing from another.  Id is optional; if Id is not
	// given, a system-wide unique Id is assigned.
	Id string

	// Thing's Model.  The default is "Thing".
	Model string

	// Thing's Name.  The default is "Thingy".
	Name string

	// [Optional] system User.  If a User is given, any browser views of
	// the Thing's UI will prompt for user/passwd.  HTTP Basic
	// Authentication is used and the user/passwd given must match the
	// system creditials for the user.  If no user is given, HTTP Basic
	// Authentication is skipped; anyone can view the UI.  The default is
	// "" (skipped).
	User string

	// [Optional] If PortPublic is non-zero, an HTTP web server is started
	// on port PortPublic.  PortPublic is typically set to 80.  The HTTP
	// web server runs Thing's UI.  The default is 0 (no web server).
	PortPublic uint

	// [Optional] If PortPublicTLS is non-zero, an HTTPS web server is
	// started on port PortPublicTLS.  PortPublicTLS is typically set to
	// 443.  The HTTPS web server will self-certify using a certificate
	// from Let's Encrypt.  The public HTTPS server will securely run the
	// Thing's UI.  If PortPublicTLS is given, PortPublic must be given.
	// The default is 0 (no web server).
	PortPublicTLS uint

	// [Optional] If PortPrivate is non-zero, a private HTTP server is
	// started on port PortPrivate.  This HTTP server does not server up
	// the Thing's UI but rather connects to Thing's Mother using a
	// websocket over HTTP.  The default is 0 (no web server).
	PortPrivate uint

	// [Optional] Run as Thing-prime.  The default is false.
	IsPrime bool

	// MaxConnection is maximum number of inbound connections to a Thing.
	// Inbound connections are WebSockets from web browsers or WebSockets
	// from Thing Prime.  The default is 30.  With the default, the 31st
	// (and higher) concurrent WebSocket connection attempt will block,
	// waiting for one of the first 30 WebSocket sessions to terminate.
	MaxConnections uint

	// Logging enable
	LoggingEnabled bool

	// ########## Mother configuration.
	//
	// This section describes a Thing's mother.  Every Thing has a mother.  A
	// mother is also a Thing.  We can build a hierarchy of Things, with a Thing
	// having a mother, a grandmother, a great grandmother, etc.
	//
	// Mother's Host address.  This the IP address or Domain Name of the
	// host running mother.  Host address can be on the local network or across
	// the internet.
	MotherHost string

	// User on host with SSH access into host.  Host should be configured
	// with user's public key so SSH access is password-less.
	MotherUser string

	// Port on Host for Mother's private HTTP server
	MotherPortPrivate uint

	// ########## Bridge configuration.
	//
	// A Thing implementing the Bridger interface will use this config for
	// bridge-specific configuration.
	//
	// Beginning bridge port number.  The bridge will listen for Thing
	// (child) connections on the port range [BeginPort-EndPort].
	//
	// The bridge port range must be within the system's
	// ip_local_reserved_ports.
	//
	// Set a range using:
	//
	//   sudo sysctl -w net.ipv4.ip_local_reserved_ports="6000-6100"
	//
	// Or, to persist setting on next boot, add to /etc/sysctl.conf:
	//
	//   net.ipv4.ip_local_reserved_ports = 6000-6100
	//
	// And then run sudo sysctl -p
	//
	BridgePortBegin uint

	// Ending bridge port number
	BridgePortEnd uint
}

type Thinger

type Thinger interface {

	// Map of Thing's subscribers, keyed by message.  On Packet receipt, a
	// subscriber is looked up by Packet message.  If there is a match, the
	// subscriber callback is called.  If no subscribers match the received
	// message, the "default" subscriber matches.  If still no matches, the
	// Packet is not handled.  If the callback is nil, the Packet is
	// (silently) dropped.  Here is an example of a subscriber map:
	//
	//	func (t *thing) Subscribers() merle.Subscribers {
	//		return merle.Subscribers{
	//			merle.CmdRun:     t.run,
	//			merle.GetState:   t.getState,
	//			merle.ReplyState: t.saveState,
	//			"SpamUpdate":     t.update,
	//			"SpamTimer":      nil,         // silent drop
	//		}
	//	}
	//
	Subscribers() Subscribers

	// Thing's web server assets.
	Assets() *ThingAssets
}

All Things implement the Thinger interface.

To be a Thinger, the Thing must implement the two methods, Subscribers() and Assets():

type thing struct {}
func (t *thing) Subscribers() merle.Subscribers { ... }
func (t *thing) Assets() *merle.ThingAssets { ... }

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL