diff --git a/RA239 Product Manual.pdf b/RA239 Product Manual.pdf new file mode 100644 index 0000000..24cd9bd Binary files /dev/null and b/RA239 Product Manual.pdf differ diff --git a/RA239.pdf b/RA239.pdf new file mode 100644 index 0000000..9336d55 Binary files /dev/null and b/RA239.pdf differ diff --git a/camera/main.go b/camera/main.go index c31badf..2ec7cb4 100644 --- a/camera/main.go +++ b/camera/main.go @@ -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 } diff --git a/config/main.go b/config/main.go index 6f4419f..cf99fa7 100644 --- a/config/main.go +++ b/config/main.go @@ -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"), + }, } } diff --git a/go.mod b/go.mod index 3a98df2..9bcaaca 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index e028bcd..5b85377 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/main.go b/main.go index fc7f1ac..18f0df5 100644 --- a/main.go +++ b/main.go @@ -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" ) diff --git a/radar/main.go b/radar/main.go index a8d37e6..9172a5a 100644 --- a/radar/main.go +++ b/radar/main.go @@ -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 +*/ diff --git a/radar_lib/configuration.go b/radar_lib/configuration.go new file mode 100644 index 0000000..0bafdf0 --- /dev/null +++ b/radar_lib/configuration.go @@ -0,0 +1 @@ +package radar_lib diff --git a/radar_lib/main.go b/radar_lib/main.go new file mode 100644 index 0000000..656861d --- /dev/null +++ b/radar_lib/main.go @@ -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 +} diff --git a/radar_lib/reader.go b/radar_lib/reader.go new file mode 100644 index 0000000..a9ad896 --- /dev/null +++ b/radar_lib/reader.go @@ -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) +} diff --git a/speedsign/main.go b/speedsign/main.go new file mode 100644 index 0000000..f4c03e3 --- /dev/null +++ b/speedsign/main.go @@ -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)) +} diff --git a/webserver/delete.go b/webserver/delete.go index c3f5719..59807c9 100644 --- a/webserver/delete.go +++ b/webserver/delete.go @@ -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") diff --git a/webserver/setup.go b/webserver/setup.go index e2869da..1f8d3c8 100644 --- a/webserver/setup.go +++ b/webserver/setup.go @@ -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) } diff --git a/webserver/test.go b/webserver/test.go index 6518d44..1caa619 100644 --- a/webserver/test.go +++ b/webserver/test.go @@ -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 {