This commit is contained in:
henne 2025-08-09 12:06:09 +02:00
parent 0eb0a3002c
commit a9c10c8332
No known key found for this signature in database
15 changed files with 407 additions and 7 deletions

BIN
RA239 Product Manual.pdf Normal file

Binary file not shown.

BIN
RA239.pdf Normal file

Binary file not shown.

View file

@ -4,6 +4,7 @@ import (
"fmt"
"os/exec"
"github.com/disintegration/imaging"
"github.com/sirupsen/logrus"
)
@ -18,6 +19,20 @@ func init() {
}
func TakePhoto(id string) error {
cmd := exec.Command(gphotoPath, "--capture-image-and-download", "--filename", fmt.Sprintf("images/original/ticket-%s.jpg", id))
return cmd.Run()
filename := fmt.Sprintf("images/original/ticket-%s.jpg", id)
cmd := exec.Command(gphotoPath, "--capture-image-and-download", "--filename", filename)
err := cmd.Run()
if err != nil {
return err
}
img, err := imaging.Open("./" + filename)
if err != nil {
return err
}
rotated := imaging.Rotate180(img)
err = imaging.Save(rotated, filename, imaging.JPEGQuality(95))
if err != nil {
return err
}
return nil
}

View file

@ -15,6 +15,10 @@ type webConfig struct {
Host string
Port int
}
type radarConfig struct {
Baud int
Port string
}
type config struct {
BaseUrl string
@ -22,6 +26,9 @@ type config struct {
Web webConfig
PrinterPort string
Radar radarConfig
SpeedsignIP string
}
var C config
@ -33,7 +40,10 @@ func init() {
flag.String("db", "db.sqlite", "Database String")
flag.String("base-url", "http://localhost:3001", "Base URL of the frontend")
flag.String("printer-port", "", "Serial port for printer")
flag.String("radar-port", "", "Radar port")
flag.Int("radar-baud", 9600, "Radar Baudrate")
flag.Int("mail-smtp-port", 587, "Mail Port")
flag.String("speedsign-ip", "", "192.168.1.143")
_ = viper.BindPFlags(flag.CommandLine)
flag.Parse()
@ -46,6 +56,7 @@ func init() {
C = config{
BaseUrl: viper.GetString("base-url"),
PrinterPort: viper.GetString("printer-port"),
SpeedsignIP: viper.GetString("speedsign-ip"),
DB: dbConfig{
Type: viper.GetString("db-type"),
ConnectionString: viper.GetString("db"),
@ -54,5 +65,9 @@ func init() {
Host: viper.GetString("host"),
Port: viper.GetInt("port"),
},
Radar: radarConfig{
Baud: viper.GetInt("radar-baud"),
Port: viper.GetString("radar-port"),
},
}
}

2
go.mod
View file

@ -54,6 +54,7 @@ require (
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.7.0 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/protobuf v1.36.1 // indirect
@ -65,6 +66,7 @@ require (
)
require (
github.com/disintegration/imaging v1.6.2
github.com/gin-contrib/sessions v1.0.2
github.com/hennedo/escpos v0.0.1
github.com/jonmol/gphoto2 v1.0.1

4
go.sum
View file

@ -20,6 +20,8 @@ github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7Do
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
@ -264,6 +266,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=

View file

@ -3,6 +3,7 @@ package main
import (
"git.ctdo.de/henne/blitzer-v2/config"
"git.ctdo.de/henne/blitzer-v2/db"
_ "git.ctdo.de/henne/blitzer-v2/radar"
"git.ctdo.de/henne/blitzer-v2/webserver"
)

View file

@ -1,5 +1,67 @@
package radar
func init() {
import (
"fmt"
"git.ctdo.de/henne/blitzer-v2/camera"
"git.ctdo.de/henne/blitzer-v2/config"
"git.ctdo.de/henne/blitzer-v2/db"
"git.ctdo.de/henne/blitzer-v2/printer"
"git.ctdo.de/henne/blitzer-v2/radar_lib"
"git.ctdo.de/henne/blitzer-v2/speedsign"
"github.com/sirupsen/logrus"
)
var r radar_lib.Radar
func init() {
r = radar_lib.New(config.C.Radar.Port, config.C.Radar.Baud)
r.SetEventHandler(onEvent)
r.SetSpeedHandler(onSpeedEvent)
}
func onSpeedEvent(speed int) {
speedsign.Show(speed)
}
func onEvent(speed int) {
speedingTicket := db.SpeedingTicket{
Speed: speed,
AllowedSpeed: db.GetConfig().TriggerSpeed,
}
if err := db.DB.Save(&speedingTicket).Error; err != nil {
logrus.Error(err)
}
if err := camera.TakePhoto(speedingTicket.ID.String()); err != nil {
logrus.Error(err)
}
speedingTicket.ImagePath = fmt.Sprintf("/images/original/ticket-%s.jpg", speedingTicket.ID)
if err := db.DB.Save(&speedingTicket).Error; err != nil {
logrus.Error(err)
}
printer.PrintTicket(speedingTicket)
}
func SetConfig(height int, angle int) {
r.SetBaseConfig(height, angle, 0)
}
func SetSpeedConfig(speed int, minDistance, maxDistance, minSpeed, maxSpeed, triggerDistance int) {
r.SetTargetSpeedConfig(radar_lib.DirectionBidirectional, minDistance*2, maxDistance*2, minSpeed, maxSpeed, speed, radar_lib.OutputLogicMostPlausible)
}
func SetBaseConfig() {
r.SetCommunicationConfig(radar_lib.PortRS485, radar_lib.Baud115200, radar_lib.OutputTypeNoOutput, radar_lib.OutputTypeNoOutput, radar_lib.OutputTypeNoOutput, 50)
}
/*
cfg.RadarAngle = form.RadarAngle
cfg.RadarHeight = form.RadarHeight
cfg.RadarMaxDistance = form.RadarMaxDistance
cfg.RadarMinDistance = form.RadarMinDistance
cfg.RadarMaxSpeed = form.RadarMaxSpeed
cfg.RadarMinSpeed = form.RadarMinSpeed
cfg.RadarWaveform = form.RadarWaveform
cfg.TriggerDistance = form.TriggerDistance
cfg.TriggerSpeed = form.TriggerSpeed
*/

View file

@ -0,0 +1 @@
package radar_lib

199
radar_lib/main.go Normal file
View file

@ -0,0 +1,199 @@
package radar_lib
import (
"errors"
"log"
"sync"
"go.bug.st/serial"
)
type configResponse struct {
CommandCode int
Success bool
}
type Direction int
type OutputLogic int
type Port int
type Baud int
type OutputType int
type TriggerMethod int
type OperatingMode int
const (
ControlPinRelay = iota
ControlPinCtrl1
ControlPinCtrl2
)
const (
DirectionBidirectional Direction = iota
DirectionIncoming
DirectionOutgoing
)
const (
TargetStateInvalid = iota
TargetStateMovingInside
TargetStateExit
TargetStateWithin
)
const (
OutputLogicLargestSpeed = iota
OutputLogicStrongestEngergy
OutputLogicMostPlausible
)
const (
PortTTL Port = iota
PortRS485
)
const (
Baud9600 Baud = iota
Baud19200
Baud57600
Baud115200
)
const (
OutputTypeNoOutput OutputType = iota
OutputTypePeriodicOutput
OutputTypeValidOutput
)
const (
TriggerMethodHigh TriggerMethod = iota
TriggerMethodLow
TriggerMethodPositivePulse
TriggerMethodNegativePulse
)
const (
OperatingModeNoOutput OperatingMode = iota
OperatingModeFirstProtocol
OperatingModeSecondProtocol
)
var (
startSequence = []byte{0x43, 0x46}
endSequence = []byte{0x0D, 0x0A}
)
func New(port string, baudrate int) Radar {
s, err := serial.Open(port, &serial.Mode{
BaudRate: baudrate,
DataBits: 8,
Parity: serial.NoParity,
})
if err != nil {
log.Fatal(err)
}
r := Radar{
port: s,
baudrate: baudrate,
configResponseChannel: make(chan configResponse),
}
go r.listenSerial()
return r
}
type Radar struct {
port serial.Port
lock sync.RWMutex
configLock sync.RWMutex
configResponseChannel chan (configResponse)
baudrate int
handler func(int)
speedHandler func(int)
}
func (r *Radar) write(data []byte) error {
r.lock.Lock()
defer r.lock.Unlock()
_, err := r.port.Write(data)
if err != nil {
return err
}
return r.port.Drain()
}
// SetPinTrigger configures a pin to trigger on the radar when an object is detected in a specific range.
// controlPin defines the Pin to trigger, distance is the target distance in 0.5 meters, outputLevel is the voltage in 0.1V steps, direction defines weather to trigger on incoming / outgoing or both
func (r *Radar) SetPinTrigger(controlPin int, distance int, outputLevel int, direction Direction) error {
data := startSequence
data = append(data, 0x00, byte(controlPin), byte(distance), byte(outputLevel), byte(direction))
data = append(data, endSequence...)
return r.write(data)
}
// SetBaseConfig configures the base configuration. height defines the mounted height in 1cm steps, angle defines the vertical angle in degrees, waveformConfig is used to differentiate 2 different radars
func (r *Radar) SetBaseConfig(height int, angle int, waveformConfig int) error {
data := startSequence
data = append(data, 0x01, byte(height), byte(angle), byte(waveformConfig), 0x00)
data = append(data, endSequence...)
return r.write(data)
}
// SetEventConfig can configure up to 8 events that will trigger a response from the radar. distances are in 0.5m, speeds are in km/h.
func (r *Radar) SetEventConfig(eventNumber, minDistance, maxDistance, minSpeed, maxSpeed, direction, state int) error {
if eventNumber < 1 || eventNumber > 1 {
return errors.New("eventNumber needs to be between 1 and 8")
}
data := startSequence
data = append(data, 0x02, byte(eventNumber), byte(minDistance), byte(maxDistance), byte(minSpeed), byte(maxSpeed), byte(direction), byte(state))
data = append(data, endSequence...)
return r.write(data)
}
func (r *Radar) SetTargetSpeedConfig(direction Direction, minDistance, maxDistance, minSpeed, maxSpeed, speeding int, outputLogic OutputLogic) error {
data := startSequence
data = append(data, 0x03, byte(direction), byte(minDistance), byte(maxDistance), byte(minSpeed), byte(maxSpeed), byte(speeding), byte(outputLogic))
data = append(data, endSequence...)
return r.write(data)
}
func (r *Radar) SetCommunicationConfig(port Port, baud Baud, speedOutput, targetOutput, triggerOutput OutputType, communicationPeriod int) error {
data := startSequence
outputProtocol := 0
switch speedOutput {
case OutputTypePeriodicOutput:
outputProtocol |= (1 << 4)
case OutputTypeValidOutput:
outputProtocol |= (1 << 5)
}
switch targetOutput {
case OutputTypePeriodicOutput:
outputProtocol |= (1 << 2)
case OutputTypeValidOutput:
outputProtocol |= (1 << 3)
}
switch triggerOutput {
case OutputTypePeriodicOutput:
outputProtocol |= (1 << 0)
case OutputTypeValidOutput:
outputProtocol |= (1 << 1)
}
data = append(data, 0x03, byte(port), byte(baud), byte(outputProtocol), byte(communicationPeriod))
data = append(data, endSequence...)
return r.write(data)
}
func (r *Radar) SetControlPinConfig(controlPin int, triggerMethod TriggerMethod, outputLevel int, triggerEvent int, span int) error {
data := startSequence
data = append(data, 0x05, byte(controlPin), byte(triggerMethod), byte(outputLevel), byte(triggerEvent), byte(span))
data = append(data, endSequence...)
return r.write(data)
}
func (r *Radar) SetLampBoardOutput(operatingMode OperatingMode, luminance int, span int) error {
data := startSequence
data = append(data, 0x06, byte(operatingMode), byte(luminance), byte(span))
data = append(data, endSequence...)
return r.write(data)
}
func (r *Radar) SetEventHandler(handler func(int)) {
r.handler = handler
}
func (r *Radar) SetSpeedHandler(handler func(int)) {
r.speedHandler = handler
}

77
radar_lib/reader.go Normal file
View file

@ -0,0 +1,77 @@
package radar_lib
import (
"log"
)
func (r *Radar) listenSerial() {
rcvBuf := make([]byte, 100)
lastIndex := 0
for {
buf := make([]byte, 100)
n, err := r.port.Read(buf)
if err != nil {
log.Fatal(err)
break
}
if n == 0 {
log.Println("\nEOF")
break
}
for i := 0; i < n; i++ {
rcvBuf[lastIndex] = buf[i]
if i > 1 && buf[i-1] == 0x0D && buf[i] == 0x0A {
r.decodeInput(rcvBuf[0 : lastIndex+1])
lastIndex = 0
} else {
lastIndex++
}
}
}
}
func (r *Radar) decodeInput(buf []byte) {
// trigger message
if len(buf) > 2 && buf[0] == 0x56 && buf[1] == 0x50 {
log.Printf("Radar Trigger Message Event %d", buf[2])
return
}
// config response
if len(buf) > 2 && buf[0] == 0x46 && buf[1] == 0x43 {
s := "FAIL"
if buf[3] == 0 {
s = "SUCCESS"
}
log.Printf("%s response: Code: %d", s, buf[2])
return
}
// speed information
if len(buf) > 2 && buf[0] == 0x56 && buf[1] == 0x52 {
dir := "incoming"
if buf[2]&(1<<(6)) != 0 {
dir = "outgoing"
}
overspeed := "no"
if buf[2]&(1<<(3)) == 1 {
overspeed = "yes"
}
valid := "no"
if buf[2]&(1<<(0)) == 1 {
valid = "yes"
}
log.Printf("Speed: %dkm/h (%s, Over: %s, Valid: %s)", buf[3], dir, overspeed, valid)
r.speedHandler(int(buf[3]))
if overspeed == "yes" && valid == "yes" {
r.handler(int(buf[3]))
}
return
}
// skip this for now
if len(buf) > 2 && buf[0] == 0x56 && buf[1] == 0x51 {
return
}
log.Printf("%# x\n", buf)
}

18
speedsign/main.go Normal file
View file

@ -0,0 +1,18 @@
package speedsign
import (
"fmt"
"net/http"
"git.ctdo.de/henne/blitzer-v2/config"
"git.ctdo.de/henne/blitzer-v2/db"
)
func Show(speed int) {
if speed > db.GetConfig().TriggerSpeed {
_, _ = http.Get("http://" + config.C.SpeedsignIP + "/api/color/FF0000")
} else {
_, _ = http.Get("http://" + config.C.SpeedsignIP + "/api/color/00FF00")
}
_, _ = http.Get(fmt.Sprintf("http://%s/api/number/%d", config.C.SpeedsignIP, speed))
}

View file

@ -1,7 +1,9 @@
package webserver
import (
"fmt"
"net/http"
"os"
"git.ctdo.de/henne/blitzer-v2/db"
"github.com/gin-gonic/gin"
@ -9,6 +11,8 @@ import (
)
func HandleDelete(ctx *gin.Context) {
filename := fmt.Sprintf("images/original/ticket-%s.jpg", ctx.Param("id"))
os.Remove(filename)
if err := db.DB.Where("id = ?", ctx.Param("id")).Delete(&db.SpeedingTicket{}).Error; err != nil {
logrus.Error(err)
ctx.String(500, "internal server error")

View file

@ -2,6 +2,7 @@ package webserver
import (
"git.ctdo.de/henne/blitzer-v2/db"
"git.ctdo.de/henne/blitzer-v2/radar"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
@ -36,4 +37,5 @@ func HandleSetupSave(ctx *gin.Context) {
if err != nil {
logrus.Error(err)
}
radar.SetConfig(cfg.RadarHeight, cfg.RadarAngle)
}

View file

@ -13,11 +13,11 @@ import (
)
func HandleTest(ctx *gin.Context) {
id := rand.Intn(500)
//id := rand.Intn(500)
speedingTicket := db.SpeedingTicket{
Speed: 20 + rand.Intn(50),
ImagePath: fmt.Sprintf("https://picsum.photos/id/%d/300/200", id),
KIImagePath: fmt.Sprintf("https://picsum.photos/id/%d/300/200", id),
Speed: 20 + rand.Intn(50),
//ImagePath: fmt.Sprintf("https://picsum.photos/id/%d/300/200", id),
//KIImagePath: fmt.Sprintf("https://picsum.photos/id/%d/300/200", id),
AllowedSpeed: db.GetConfig().TriggerSpeed,
}
if err := db.DB.Save(&speedingTicket).Error; err != nil {