Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
The MIT License (MIT)
Copyright © 2016 Iori MIZUTANI <iori.mizutani@gmail.com>
Copyright © 2025 Iori MIZUTANI <iori.mizutani@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the “Software”), to deal
Expand Down
33 changes: 28 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,35 @@ Mizutani, I., & Mitsugi, J. (2016). A Multicode and Portable RFID Tag Events Emu

# Installation

Install [dep](https://github.com/golang/dep) in your system first.
## From Source

```bash
# Install the latest version
go install github.com/iomz/golemu/cmd/golemu@latest

# Or install from a local clone
git clone https://github.com/iomz/golemu.git
cd golemu
go install ./cmd/golemu

# Verify installation
golemu --help
```
$ go get github.com/iomz/golemu
$ cd $GOPATH/src/github.com/iomz/golemu
$ dep ensure && go install .

**Note:** Make sure `$GOPATH/bin` or `$HOME/go/bin` is in your `PATH` environment variable to use the `golemu` command directly.

## Build Locally

```bash
# Clone the repository
git clone https://github.com/iomz/golemu.git
cd golemu

# Build the binary
go build -o golemu ./cmd/golemu

# Run directly
./golemu --help
```

# Synopsis
Expand Down Expand Up @@ -98,4 +121,4 @@ See the LICENSE file.

## Author

Iori Mizutani (iomz)
Iori Mizutani (@iomz)
128 changes: 128 additions & 0 deletions api/handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
//
// Use of this source code is governed by The MIT License
// that can be found in the LICENSE file.

package api

import (
"net/http"

"github.com/fatih/structs"
"github.com/gin-gonic/gin"
"github.com/iomz/go-llrp"
"github.com/iomz/golemu/tag"
log "github.com/sirupsen/logrus"
)

// Handler handles API requests
type Handler struct {
tagManagerChan chan tag.Manager
}

// NewHandler creates a new API handler
func NewHandler(tagManagerChan chan tag.Manager) *Handler {
return &Handler{
tagManagerChan: tagManagerChan,
}
}

// PostTag handles tag addition requests
func (h *Handler) PostTag(c *gin.Context) {
var json []llrp.TagRecord
if err := c.ShouldBindJSON(&json); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request", "details": err.Error()})
return
}

if res := h.reqAddTag(json); res == "error" {
c.JSON(http.StatusConflict, gin.H{"error": "One or more tags already exist"})
} else {
c.JSON(http.StatusCreated, gin.H{"message": "Tags added successfully"})
}
}

// DeleteTag handles tag deletion requests
func (h *Handler) DeleteTag(c *gin.Context) {
var json []llrp.TagRecord
if err := c.ShouldBindJSON(&json); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request", "details": err.Error()})
return
}

if res := h.reqDeleteTag(json); res == "error" {
c.JSON(http.StatusNotFound, gin.H{"error": "One or more tags not found"})
} else {
c.JSON(http.StatusOK, gin.H{"message": "Tags deleted successfully"})
}
}

// GetTags handles tag retrieval requests
func (h *Handler) GetTags(c *gin.Context) {
tagList := h.reqRetrieveTag()
c.JSON(http.StatusOK, tagList)
}

func (h *Handler) reqAddTag(req []llrp.TagRecord) string {
hasError := false
for _, t := range req {
tagObj, err := llrp.NewTag(&llrp.TagRecord{
PCBits: t.PCBits,
EPC: t.EPC,
})
if err != nil {
log.Errorf("error creating tag: %v", err)
hasError = true
continue
}

add := tag.Manager{
Action: tag.AddTags,
Tags: []*llrp.Tag{tagObj},
}
h.tagManagerChan <- add
}

if hasError {
return "error"
}
log.Debugf("add %v", req)
return "add"
}

func (h *Handler) reqDeleteTag(req []llrp.TagRecord) string {
for _, t := range req {
tagObj, err := llrp.NewTag(&llrp.TagRecord{
PCBits: t.PCBits,
EPC: t.EPC,
})
if err != nil {
log.Error(err)
continue
}

deleteCmd := tag.Manager{
Action: tag.DeleteTags,
Tags: []*llrp.Tag{tagObj},
}
h.tagManagerChan <- deleteCmd
}

log.Debugf("delete %v", req)
return "delete"
}

func (h *Handler) reqRetrieveTag() []map[string]interface{} {
retrieve := tag.Manager{
Action: tag.RetrieveTags,
Tags: []*llrp.Tag{},
}
h.tagManagerChan <- retrieve
retrieve = <-h.tagManagerChan
var tagList []map[string]interface{}
for _, tagObj := range retrieve.Tags {
t := structs.Map(llrp.NewTagRecord(*tagObj))
tagList = append(tagList, t)
}
log.Debugf("retrieve: %v", tagList)
return tagList
}
38 changes: 38 additions & 0 deletions api/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// Use of this source code is governed by The MIT License
// that can be found in the LICENSE file.

package api

import (
"strconv"

"github.com/gin-gonic/gin"
"github.com/iomz/golemu/tag"
)

// Server represents the API server
type Server struct {
handler *Handler
port int
}

// NewServer creates a new API server
func NewServer(port int, tagManagerChan chan tag.Manager) *Server {
return &Server{
handler: NewHandler(tagManagerChan),
port: port,
}
}

// Start starts the API server
func (s *Server) Start() error {
r := gin.Default()
v1 := r.Group("api/v1")
{
v1.POST("/tags", s.handler.PostTag)
v1.DELETE("/tags", s.handler.DeleteTag)
v1.GET("/tags", s.handler.GetTags)
}
return r.Run(":" + strconv.Itoa(s.port))
}
62 changes: 62 additions & 0 deletions cmd/golemu/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//
// Use of this source code is governed by The MIT License
// that can be found in the LICENSE file.

package main

import (
"os"

"github.com/gin-gonic/gin"
"github.com/iomz/golemu/config"
"github.com/iomz/golemu/connection"
"github.com/iomz/golemu/server"
log "github.com/sirupsen/logrus"
"gopkg.in/alecthomas/kingpin.v2"
)

func main() {
// Set version
config.App.Version(config.Version)
parse := kingpin.MustParse(config.App.Parse(os.Args[1:]))

// Set up logrus
log.SetLevel(log.InfoLevel)

cfg := config.GetConfig()
if cfg.Debug {
gin.SetMode(gin.DebugMode)
log.SetLevel(log.DebugLevel)
} else {
gin.SetMode(gin.ReleaseMode)
}

switch parse {
case config.Client.FullCommand():
client := connection.NewClient(cfg.IP.String(), cfg.Port)
os.Exit(client.Run())
case config.Server.FullCommand():
srv := server.NewServer(
cfg.IP.String(),
cfg.Port,
cfg.APIPort,
cfg.PDU,
cfg.ReportInterval,
cfg.KeepaliveInterval,
cfg.InitialMessageID,
cfg.File,
)
os.Exit(srv.Run())
case config.Simulator.FullCommand():
sim := connection.NewSimulator(
cfg.IP.String(),
cfg.Port,
cfg.PDU,
cfg.ReportInterval,
cfg.SimulationDir,
cfg.InitialMessageID,
)
sim.Run()
}
}

73 changes: 73 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//
// Use of this source code is governed by The MIT License
// that can be found in the LICENSE file.

package config

import (
"net"

"gopkg.in/alecthomas/kingpin.v2"
)

var (
// Version is the current version
Version = "0.2.0"

// App is the kingpin application
App = kingpin.New("golemu", "A mock LLRP-based logical reader emulator for RFID Tags.")

// Global flags
Debug = App.Flag("debug", "Enable debug mode.").Short('v').Default("false").Bool()
InitialMessageID = App.Flag("initialMessageID", "The initial messageID to start from.").Default("1000").Int()
InitialKeepaliveID = App.Flag("initialKeepaliveID", "The initial keepaliveID to start from.").Default("80000").Int()
IP = App.Flag("ip", "LLRP listening address.").Short('a').Default("0.0.0.0").IP()
KeepaliveInterval = App.Flag("keepalive", "LLRP Keepalive interval.").Short('k').Default("0").Int()
Port = App.Flag("port", "LLRP listening port.").Short('p').Default("5084").Int()
PDU = App.Flag("pdu", "The maximum size of LLRP PDU.").Short('m').Default("1500").Int()
ReportInterval = App.Flag("reportInterval", "The interval of ROAccessReport in ms. Pseudo ROReport spec option.").Short('i').Default("10000").Int()

// Client mode
Client = App.Command("client", "Run as an LLRP client; connect to an LLRP server and receive events (test-only).")

// Server mode
Server = App.Command("server", "Run as an LLRP tag stream server.")
APIPort = Server.Flag("apiPort", "The port for the API endpoint.").Default("3000").Int()
File = Server.Flag("file", "The file containing Tag data.").Short('f').Default("tags.gob").String()

// Simulator mode
Simulator = App.Command("simulator", "Run in the simulator mode.")
SimulationDir = Simulator.Arg("simulationDir", "The directory contains tags for each event cycle.").Required().String()
)

// Config holds the application configuration
type Config struct {
Debug bool
InitialMessageID int
InitialKeepaliveID int
IP net.IP
KeepaliveInterval int
Port int
PDU int
ReportInterval int
APIPort int
File string
SimulationDir string
}

// GetConfig returns the parsed configuration
func GetConfig() *Config {
return &Config{
Debug: *Debug,
InitialMessageID: *InitialMessageID,
InitialKeepaliveID: *InitialKeepaliveID,
IP: *IP,
KeepaliveInterval: *KeepaliveInterval,
Port: *Port,
PDU: *PDU,
ReportInterval: *ReportInterval,
APIPort: *APIPort,
File: *File,
SimulationDir: *SimulationDir,
}
}
Loading
Loading