先介紹一下項目的背景,之前單位有一個做小型快遞分揀機的需求,針對小型包裹智能分揀到不通的出口。大致的物理傳送帶如下方圖所示,原諒我不會畫圖。此文章的目的,只是給大家展示一下golang channel的用處。
如上圖所示,傳送帶分了幾個部分,頭部區域,分揀工作區域,硬件設備(傳感器和臂手)。
頭部區域主要有攝像頭和掃碼槍,主要是識別包裹,查詢出包裹對應的區域地址。
頭部區域和分揀工作區域邊界,會有一個紅外線傳感器,來確定包裹進入了分揀工作區域。
在傳送帶的齒輪上會有一個速度傳感器,來實時接受信號,計算傳送帶轉動的距離。
分揀工作區域每隔30cm會有臂手(這裏我們會有led燈做模擬,其實就是一個GPIO)
根據以上的簡述,我們用golang代碼來簡單實現這個邏輯
1. 功能分析
頭部區域涉及到掃描槍和攝像頭的AI識別,我們就暫時用一個scanPacket函數來模擬代替,
// 模擬 每隔兩秒鐘會有一個包裹
func ScanPacket() {
ticker := time.NewTicker(2 * time.Second)
for {
<-ticker.C
fmt.Println("scan a packet")
}
}
當識別到一個包裹後我們就要確定它要在哪個led燈附近,所以我們先要把led的配置初始化好
// Light 燈
type Light struct {
Id int // 燈編號
State string // on/off
Distance int64 // 距離入口紅外線傳感器的位置(就是距分揀工作區域起始位置的長度) 單位 mm
SwitchCh chan struct{} // 用來通知該燈亮起
}
var lights = map[int]*Light{
1: {
Id: 1,
State: "off",
Distance: 300,
SwitchCh: make(chan struct{}, 10),
},
2: {
Id: 2,
State: "off",
Distance: 600,
SwitchCh: make(chan struct{}, 10),
},
3: {
Id: 3,
State: "off",
Distance: 900,
SwitchCh: make(chan struct{}, 10),
},
4: {
Id: 4,
State: "off",
Distance: 1200,
SwitchCh: make(chan struct{}, 10),
},
}
// 此處模擬了4個led燈和對應的傳送帶的位置
定義包裹的結構體
// Packet 包裹
type Packet struct {
Id int64 // 包裹id
BelongLight *Light // 所屬led燈的位置
Distance int64 // 這個包裹對應的分揀工作區的位置(就是燈的位置)
SensorCh chan struct{} // 傳感器的channel
}
由於速度傳感器的io頻率很高,如果把所有的packet都維護到一個數組裏面,後面計算每個包裹的距離時,鎖的併發會很大,我這邊就利用了goroutine的優勢,對每個包裹啓動了一個goroutine,包裹的狀態和距離都是單協程計算,不存在數據衝突。
func packetWorker(packet *Packet) {
//fmt.Printf("packet %d scan\n", packet.Id)
defer func() {
close(packet.SensorCh)
}()
// 1. 註冊包裹的channel
packetChanRegisterSets.Register(packet)
// 2. 開始監控速度傳感器的信號
for {
<-packet.SensorCh
packet.Distance = packet.Distance - 8
//fmt.Printf("packet %d distance is %d\n", packet.Id, packet.Distance)
if packet.Distance >= -16 && packet.Distance <= 16 {
// 3. 通知對應的led燈亮起/(臂手撥動)
packet.BelongLight.SwitchCh <- struct{}{}
// 4. 取消註冊
packetChanRegisterSets.UnRegister(packet)
return
}
}
}
每個包裹實例都有一個channel, 把包裹所有的channel都註冊到一個集合裏面,當接受速度傳感器信號時,只需要把集合內的所有channel發一個信號(廣播),就能通知所有的包裹重新計算所到的位置。整個系統的併發全部集中到PacketChanRegisterSet,大大的縮小了併發的範圍。大部分的併發也只是讀
type PacketChanRegisterSet struct {
Set sync.Map
}
func (s *PacketChanRegisterSet) Register(packet *Packet) {
s.Set.Store(packet.Id, packet.SensorCh)
}
// Broadcast 廣播
func (s *PacketChanRegisterSet) Broadcast() {
s.Set.Range(func(key, value any) bool {
c := value.(chan struct{})
c <- struct{}{}
return true
})
}
func (s *PacketChanRegisterSet) UnRegister(packet *Packet) {
s.Set.Delete(packet.Id)
}
當led燈(或者臂手)接受到channel信號的時候就亮起
func lightWorker(light *Light) {
for {
<-light.SwitchCh
fmt.Printf("light %d is turn on\n", light.Id)
}
}
2. 代碼實現
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
var lights = map[int]*Light{
1: {
Id: 1,
State: "off",
Distance: 300,
SwitchCh: make(chan struct{}, 10),
},
2: {
Id: 2,
State: "off",
Distance: 600,
SwitchCh: make(chan struct{}, 10),
},
3: {
Id: 3,
State: "off",
Distance: 900,
SwitchCh: make(chan struct{}, 10),
},
4: {
Id: 4,
State: "off",
Distance: 1200,
SwitchCh: make(chan struct{}, 10),
},
}
type PacketChanRegisterSet struct {
Set sync.Map
}
func (s *PacketChanRegisterSet) Register(packet *Packet) {
s.Set.Store(packet.Id, packet.SensorCh)
}
// Broadcast 廣播
func (s *PacketChanRegisterSet) Broadcast() {
s.Set.Range(func(key, value any) bool {
c := value.(chan struct{})
c <- struct{}{}
return true
})
}
func (s *PacketChanRegisterSet) UnRegister(packet *Packet) {
s.Set.Delete(packet.Id)
}
var packetChanRegisterSets = PacketChanRegisterSet{
Set: sync.Map{},
}
func main() {
// 啓動燈
for _, light := range lights {
go lightWorker(light)
}
// 啓動掃描包裹程序
go ScanPacket()
// 模擬傳感器
ticker := time.NewTicker(200 * time.Millisecond)
for {
<-ticker.C
packetChanRegisterSets.Broadcast()
}
}
// Packet 包裹
type Packet struct {
Id int64 // 包裹id
BelongLight *Light // 所屬led燈的位置
Distance int64 // 這個包裹對應的分揀工作區的位置(就是燈的位置)
SensorCh chan struct{} // 傳感器的channel
}
// Light 燈
type Light struct {
Id int // 燈編號
State string // on/off
Distance int64 // 距離 單位 mm
SwitchCh chan struct{}
}
// ScanPacket 掃描包裹
func ScanPacket() {
ticker := time.NewTicker(2 * time.Second)
for {
<-ticker.C
fmt.Println("scan a packet")
id := rand.Intn(4)
light := lights[id+1]
packet := Packet{
Id: time.Now().Unix(),
BelongLight: light,
Distance: light.Distance,
SensorCh: make(chan struct{}, 100),
}
go packetWorker(&packet)
}
}
func packetWorker(packet *Packet) {
//fmt.Printf("packet %d scan\n", packet.Id)
defer func() {
close(packet.SensorCh)
}()
// 1. 註冊包裹的channel
packetChanRegisterSets.Register(packet)
// 2. 開始監控速度傳感器的信號
for {
<-packet.SensorCh
packet.Distance = packet.Distance - 8
//fmt.Printf("packet %d distance is %d\n", packet.Id, packet.Distance)
if packet.Distance >= -16 && packet.Distance <= 16 {
// 3. 通知對應的led燈亮起/(臂手撥動)
packet.BelongLight.SwitchCh <- struct{}{}
// 4. 取消註冊
packetChanRegisterSets.UnRegister(packet)
return
}
}
}
func lightWorker(light *Light) {
for {
<-light.SwitchCh
fmt.Printf("light %d is turn on\n", light.Id)
}
}