chore: init bj_power monorepo (dashboard/mes/wms/wms_client/workstation), clear subproject git metadata and align naming to folder names

This commit is contained in:
SunYF
2026-08-27 10:09:54 +08:00
commit 1f0ee844a4
408 changed files with 102201 additions and 0 deletions
+242
View File
@@ -0,0 +1,242 @@
package api
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
)
type Client struct {
baseURL string
client *http.Client
}
func NewClient(baseURL string) *Client {
return &Client{
baseURL: baseURL,
client: &http.Client{},
}
}
type APIResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
type WorkOrderInfo struct {
WorkOrderNo string `json:"workOrderNo"`
WorkpieceType string `json:"workpieceType"`
WorkpieceTypeName string `json:"workpieceTypeName"`
WorkpieceCount int `json:"workpieceCount"`
Status string `json:"status"`
StatusName string `json:"statusName"`
CreatedAt string `json:"createdAt"`
}
type MeasurementRecord struct {
ID int `json:"id"`
WorkOrderNo string `json:"workOrderNo"`
WorkpieceNo string `json:"workpieceNo"`
Category string `json:"category"`
WorkpieceType string `json:"workpieceType"`
ValueA float64 `json:"valueA"`
ValueB float64 `json:"valueB"`
ValueC float64 `json:"valueC"`
Unit string `json:"unit"`
CreatedAt string `json:"createdAt"`
}
type MeasurementListResponse struct {
List []MeasurementRecord `json:"list"`
Total int `json:"total"`
Page int `json:"page"`
Limit int `json:"limit"`
}
type SubmitRequest struct {
WorkOrderNo string `json:"workOrderNo"`
WorkpieceNo string `json:"workpieceNo"`
Category string `json:"category"`
WorkpieceType string `json:"workpieceType"`
ValueA float64 `json:"valueA"`
ValueB *float64 `json:"valueB,omitempty"`
ValueC *float64 `json:"valueC,omitempty"`
Unit string `json:"unit"`
Serial string `json:"serial"`
Timestamp string `json:"timestamp"`
}
type SubmitResponse struct {
ID int `json:"id"`
}
func (c *Client) QueryWorkOrder(ctx context.Context, workOrderNo string) (*WorkOrderInfo, error) {
u, err := url.Parse(c.baseURL + "/api/v1/micrometer/work-order/query")
if err != nil {
return nil, err
}
q := u.Query()
q.Set("workOrderNo", workOrderNo)
u.RawQuery = q.Encode()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var apiResp APIResponse
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
return nil, err
}
if apiResp.Code != 0 {
return nil, fmt.Errorf("API error: %s", apiResp.Message)
}
data, err := json.Marshal(apiResp.Data)
if err != nil {
return nil, err
}
var result WorkOrderInfo
if err := json.Unmarshal(data, &result); err != nil {
return nil, err
}
return &result, nil
}
func (c *Client) QueryMeasurements(ctx context.Context, workOrderNo, workpieceNo string, page, limit int) (*MeasurementListResponse, error) {
u, err := url.Parse(c.baseURL + "/api/v1/micrometer/measurements")
if err != nil {
return nil, err
}
q := u.Query()
q.Set("workOrderNo", workOrderNo)
if workpieceNo != "" {
q.Set("workpieceNo", workpieceNo)
}
q.Set("page", fmt.Sprintf("%d", page))
q.Set("limit", fmt.Sprintf("%d", limit))
u.RawQuery = q.Encode()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var apiResp APIResponse
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
return nil, err
}
if apiResp.Code != 0 {
return nil, fmt.Errorf("API error: %s", apiResp.Message)
}
data, err := json.Marshal(apiResp.Data)
if err != nil {
return nil, err
}
var result MeasurementListResponse
if err := json.Unmarshal(data, &result); err != nil {
return nil, err
}
return &result, nil
}
func (c *Client) SubmitMeasurement(ctx context.Context, req SubmitRequest) (*SubmitResponse, error) {
body, err := json.Marshal(req)
if err != nil {
return nil, err
}
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/api/v1/micrometer/measurements", bytes.NewReader(body))
if err != nil {
return nil, err
}
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(httpReq)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var apiResp APIResponse
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
return nil, err
}
if apiResp.Code != 0 {
return nil, fmt.Errorf("API error: %s", apiResp.Message)
}
data, err := json.Marshal(apiResp.Data)
if err != nil {
return nil, err
}
var result SubmitResponse
if err := json.Unmarshal(data, &result); err != nil {
return nil, err
}
return &result, nil
}
func (c *Client) QueryWorkOrderList(ctx context.Context, keyword string, limit int) ([]WorkOrderOption, error) {
u, err := url.Parse(c.baseURL + "/api/v1/micrometer/work-orders")
if err != nil {
return nil, err
}
q := u.Query()
if keyword != "" {
q.Set("keyword", keyword)
}
q.Set("limit", fmt.Sprintf("%d", limit))
u.RawQuery = q.Encode()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var apiResp APIResponse
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
return nil, err
}
if apiResp.Code != 0 {
return nil, fmt.Errorf("API error: %s", apiResp.Message)
}
data, err := json.Marshal(apiResp.Data)
if err != nil {
return nil, err
}
var result []WorkOrderOption
if err := json.Unmarshal(data, &result); err != nil {
return nil, err
}
return result, nil
}
type WorkOrderOption struct {
WorkOrderNo string `json:"workOrderNo"`
Description string `json:"description"`
}
+115
View File
@@ -0,0 +1,115 @@
package app
import (
"fmt"
"bjhardman.cn/bjhardman/hougai_insize_micrometer/insize"
)
type Unit int8
const (
UnitMM Unit = 0
UnitInch Unit = 1
)
const MmPerInch = 25.4
type Collector struct {
values []float64
filled []bool
current int
serial uint32
lastSeq byte
itemDefs []string
}
func NewCollector(itemDefs []string) *Collector {
n := len(itemDefs)
return &Collector{
values: make([]float64, n),
filled: make([]bool, n),
current: 0,
itemDefs: itemDefs,
}
}
func (c *Collector) ApplyFrame(f insize.Frame) {
mm := f.Value
if f.Unit == insize.UnitInch {
mm = f.Value * MmPerInch
}
c.values[c.current] = mm
c.filled[c.current] = true
c.serial = f.SerialNo
c.lastSeq = f.SeqNo
if c.current < len(c.values)-1 {
c.current++
}
}
func (c *Collector) SetValue(i int, mm float64) {
if i < 0 || i >= len(c.values) {
return
}
c.values[i] = mm
c.filled[i] = true
c.current = i
}
func (c *Collector) SetCurrent(i int) {
if i >= 0 && i < len(c.values) {
c.current = i
}
}
func (c *Collector) CurrentIndex() int { return c.current }
func (c *Collector) DisplayValue(i int, u Unit) (string, bool) {
if i < 0 || i >= len(c.values) || !c.filled[i] {
return "----", false
}
mm := c.values[i]
if u == UnitInch {
return fmt.Sprintf("%.5f", mm/MmPerInch), true
}
return fmt.Sprintf("%.3f", mm), true
}
func (c *Collector) AllFilled() bool {
for _, ok := range c.filled {
if !ok {
return false
}
}
return true
}
func (c *Collector) Clear() {
for i := range c.values {
c.values[i] = 0
c.filled[i] = false
}
c.current = 0
c.serial = 0
c.lastSeq = 0
}
func (c *Collector) Serial() (uint32, byte) {
return c.serial, c.lastSeq
}
func (c *Collector) Value(i int) float64 {
if i < 0 || i >= len(c.values) {
return 0
}
return c.values[i]
}
func (c *Collector) Count() int {
return len(c.values)
}
func (c *Collector) ItemDefs() []string {
return c.itemDefs
}
@@ -0,0 +1,19 @@
//go:build !windows
package autostart
func CreateDesktopShortcut(appName string) error {
return nil
}
func Enable(appName string) error {
return nil
}
func Disable(appName string) error {
return nil
}
func IsEnabled(appName string) bool {
return false
}
@@ -0,0 +1,76 @@
package autostart
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"golang.org/x/sys/windows/registry"
)
func Enable(appName string) error {
exePath, err := os.Executable()
if err != nil {
return fmt.Errorf("获取可执行文件路径失败: %w", err)
}
exePath, err = filepath.Abs(exePath)
if err != nil {
return fmt.Errorf("获取绝对路径失败: %w", err)
}
k, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Run`, registry.ALL_ACCESS)
if err != nil {
return fmt.Errorf("打开注册表失败: %w", err)
}
defer k.Close()
if err := k.SetStringValue(appName, exePath); err != nil {
return fmt.Errorf("设置注册表值失败: %w", err)
}
return nil
}
func Disable(appName string) error {
k, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Run`, registry.ALL_ACCESS)
if err != nil {
return fmt.Errorf("打开注册表失败: %w", err)
}
defer k.Close()
if err := k.DeleteValue(appName); err != nil {
if err == registry.ErrNotExist {
return nil
}
return fmt.Errorf("删除注册表值失败: %w", err)
}
return nil
}
func IsEnabled(appName string) bool {
k, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Run`, registry.READ)
if err != nil {
return false
}
defer k.Close()
_, _, err = k.GetStringValue(appName)
return err == nil
}
func CreateDesktopShortcut(appName string) error {
exePath, err := os.Executable()
if err != nil {
return fmt.Errorf("获取exe路径失败: %w", err)
}
exePath, _ = filepath.Abs(exePath)
home, _ := os.UserHomeDir()
shortcutPath := filepath.Join(home, "Desktop", appName+".lnk")
psCmd := fmt.Sprintf(
`$ws = New-Object -ComObject WScript.Shell; $s = $ws.CreateShortcut('%s'); $s.TargetPath = '%s'; $s.WorkingDirectory = '%s'; $s.Save()`,
shortcutPath, exePath, filepath.Dir(exePath))
cmd := exec.Command("powershell", "-NoProfile", "-Command", psCmd)
return cmd.Run()
}
+44
View File
@@ -0,0 +1,44 @@
package config
import (
"encoding/json"
"os"
)
type Config struct {
APIURL string `json:"api_url"`
SerialPort string `json:"serial_port"`
BaudRate int `json:"baud_rate"`
DisplayUnit string `json:"display_unit"`
}
func Load(path string) (Config, error) {
cfg := Config{
APIURL: "http://localhost:8888",
SerialPort: "COM7",
BaudRate: 115200,
DisplayUnit: "mm",
}
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
if err := cfg.Save(path); err != nil {
return cfg, err
}
return cfg, nil
}
return cfg, err
}
if err := json.Unmarshal(data, &cfg); err != nil {
return cfg, err
}
return cfg, nil
}
func (c Config) Save(path string) error {
data, err := json.MarshalIndent(c, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, data, 0644)
}
+77
View File
@@ -0,0 +1,77 @@
package serialport
import (
"fmt"
"io"
"log"
"time"
"go.bug.st/serial"
"bjhardman.cn/bjhardman/hougai_insize_micrometer/insize"
)
type Port struct {
port serial.Port
frames chan insize.Frame
errs chan error
done chan struct{}
}
func Open(name string, baud int) (*Port, error) {
mode := &serial.Mode{
BaudRate: baud,
DataBits: 8,
Parity: serial.NoParity,
StopBits: serial.OneStopBit,
}
sp, err := serial.Open(name, mode)
if err != nil {
return nil, fmt.Errorf("打开串口 %s: %w", name, err)
}
if err := sp.SetReadTimeout(500 * time.Millisecond); err != nil {
}
p := &Port{
port: sp,
frames: make(chan insize.Frame, 16),
errs: make(chan error, 4),
done: make(chan struct{}),
}
go p.readLoop()
return p, nil
}
func (p *Port) Frames() <-chan insize.Frame { return p.frames }
func (p *Port) Errors() <-chan error { return p.errs }
func (p *Port) Close() error {
close(p.done)
return p.port.Close()
}
func (p *Port) readLoop() {
reader := insize.NewReader(p.port)
for {
select {
case <-p.done:
return
default:
}
f, err := reader.ReadFrame()
if err != nil {
if err == io.EOF || err == io.ErrUnexpectedEOF {
p.errs <- fmt.Errorf("串口读取结束: %w", err)
return
}
log.Printf("serialport: 跳过非法帧: %v", err)
continue
}
select {
case p.frames <- f:
case <-p.done:
return
}
}
}
File diff suppressed because it is too large Load Diff
+172
View File
@@ -0,0 +1,172 @@
package ui
import (
"strconv"
"strings"
"github.com/ying32/govcl/vcl"
"github.com/ying32/govcl/vcl/types"
)
const (
keyBtnSize int32 = 96
keyFontSize int32 = 24
dispFontSize int32 = 32
gap int32 = 10
margin int32 = 16
)
// showNumKeyPad 弹出触屏数字键盘,返回(输入字符串, 是否确认)。
// initial 为初始显示值;owner 通常为主窗体。
// 布局:顶部显示框 + 左侧 4×3 数字网格 + 右侧纵向整列(退格/清空) + 底部整行(取消/确认)。
func showNumKeyPad(owner vcl.IComponent, initial string) (string, bool) {
form := vcl.NewForm(owner)
form.SetCaption("输入数值")
form.SetPosition(types.PoScreenCenter)
form.SetBorderStyle(types.BsDialog)
form.SetOnClose(func(sender vcl.IObject, action *types.TCloseAction) {
*action = types.CaFree
})
// 显示框高度随字号。
dispH := dispFontSize + 24
// 左侧 4 列 x 3 行数字网格。
gridCols := int32(4)
gridRows := int32(3)
gridW := gridCols*keyBtnSize + (gridCols-1)*gap
gridH := gridRows*keyBtnSize + (gridRows-1)*gap
// 右侧功能键列宽与数字键同宽,纵向整列高度与网格同高,两个按钮均分。
sideW := keyBtnSize
sideH := gridH
// 底部整行按钮高度与数字键同高。
bottomH := keyBtnSize
formW := margin*2 + gridW + gap + sideW
formH := margin + dispH + gap + gridH + gap + bottomH + margin
form.SetWidth(formW)
form.SetHeight(formH)
// 显示框:横跨整个宽度。
display := vcl.NewEdit(form)
display.SetParent(form)
display.SetLeft(margin)
display.SetTop(margin)
display.SetWidth(formW - margin*2)
display.SetHeight(dispH)
display.Font().SetSize(dispFontSize)
display.SetText(initial)
display.SetReadOnly(true)
display.SetAlignment(types.TaRightJustify)
var (
result string
confirmed bool
)
curText := func() string {
var s string
display.GetTextBuf(&s, 64)
return s
}
setText := func(s string) { display.SetText(s) }
makeBtn := func(caption string, l, t, w, h int32) *vcl.TButton {
b := vcl.NewButton(form)
b.SetParent(form)
b.SetLeft(l)
b.SetTop(t)
b.SetWidth(w)
b.SetHeight(h)
b.SetCaption(caption)
b.Font().SetSize(keyFontSize)
return b
}
gridTop := margin + dispH + gap
gridLeft := margin
// 数字键 4 列 x 3 行:
// [7][8][9][.]
// [4][5][6][0]
// [1][2][3][+/-]
layout := []string{"7", "8", "9", ".", "4", "5", "6", "0", "1", "2", "3", "+/-"}
for i, cap := range layout {
col := int32(i % int(gridCols))
row := int32(i / int(gridCols))
btn := makeBtn(cap,
gridLeft+col*(keyBtnSize+gap),
gridTop+row*(keyBtnSize+gap),
keyBtnSize, keyBtnSize)
btn.SetOnClick(func(sender vcl.IObject) {
c := vcl.AsButton(sender).Caption()
s := curText()
switch c {
case "+/-":
if strings.HasPrefix(s, "-") {
s = s[1:]
} else if s != "" {
s = "-" + s
}
case ".":
if !strings.Contains(s, ".") {
if s == "" || s == "-" {
s += "0."
} else {
s += "."
}
}
default:
s += c
}
setText(s)
})
}
// 右侧纵向整列:退格 / 清空,均分高度。
sideLeft := margin + gridW + gap
sideBtnH := (sideH - gap) / 2 // 两按钮间留一个 gap
sideBtns := []struct {
cap string
fn func()
}{
{"退格", func() {
s := curText()
if len(s) > 0 {
s = s[:len(s)-1]
}
setText(s)
}},
{"清空", func() { setText("") }},
}
for i, b := range sideBtns {
btn := makeBtn(b.cap, sideLeft, gridTop+int32(i)*(sideBtnH+gap), sideW, sideBtnH)
btn.SetOnClick(func(sender vcl.IObject) { b.fn() })
}
// 底部整行:取消 / 确认,均分宽度。
bottomTop := gridTop + gridH + gap
bottomW := (formW - margin*2 - gap) / 2 // 两按钮间留一个 gap
cancel := makeBtn("取消", margin, bottomTop, bottomW, bottomH)
cancel.SetOnClick(func(sender vcl.IObject) {
confirmed = false
form.Close()
})
ok := makeBtn("确认", margin+bottomW+gap, bottomTop, bottomW, bottomH)
ok.Font().SetSize(keyFontSize + 4)
ok.SetOnClick(func(sender vcl.IObject) {
s := curText()
if s == "" || s == "-" {
confirmed = false
} else if _, err := strconv.ParseFloat(s, 64); err != nil {
vcl.ShowMessage("请输入合法数值")
return
} else {
result = s
confirmed = true
}
form.Close()
})
form.ShowModal()
return result, confirmed
}