smartScaleService.ts 3.91 KB
export type SmartScaleReadKind = 'tared' | 'gross'

const STORAGE_IP = 'smart_scale_ip'
const STORAGE_PORT = 'smart_scale_port'
const DEFAULT_IP = '127.0.0.1'
const DEFAULT_PORT = 6900
const READ_TIMEOUT_MS = 4000

type MoeTcpPlugin = {
  connect: (opts: { ip: string; port: number }, cb: (res: { code?: number; msg?: string }) => void) => void
  disconnect: () => void
  sendStr: (opts: { message: string }) => void
  onReceive: (cb: (res: { code?: number; data?: string; msg?: string }) => void) => void
  onDisconnect: (cb: (res: unknown) => void) => void
}

function getTcpPlugin(): MoeTcpPlugin | null {
  // #ifdef APP-PLUS
  try {
    const u = uni as any
    return u?.requireNativePlugin ? (u.requireNativePlugin('moe-tcp-client') as MoeTcpPlugin) : null
  } catch {
    return null
  }
  // #endif
  // #ifndef APP-PLUS
  return null
  // #endif
}

function readScaleSettings(): { ip: string; port: number } {
  let ip = DEFAULT_IP
  let port = DEFAULT_PORT
  try {
    const storedIp = uni.getStorageSync(STORAGE_IP)
    const storedPort = uni.getStorageSync(STORAGE_PORT)
    if (storedIp) ip = String(storedIp).trim() || ip
    const p = Number(storedPort)
    if (Number.isFinite(p) && p > 0) port = p
  } catch {
    /* ignore */
  }
  return { ip, port }
}

function parseWeightFromMessage(msg: string): string | null {
  const text = String(msg ?? '').replace(/,/g, '.')
  const matches = text.match(/-?\d+(?:\.\d+)?/g)
  if (!matches?.length) return null
  const last = matches[matches.length - 1]
  const n = Number(last)
  if (!Number.isFinite(n)) return null
  return String(n)
}

function connectTcp(plugin: MoeTcpPlugin, ip: string, port: number): Promise<void> {
  return new Promise((resolve, reject) => {
    plugin.connect({ ip, port }, (res) => {
      if (res?.code === 1) resolve()
      else reject(new Error(res?.msg || 'Could not connect to smart scale.'))
    })
  })
}

/** Read weight from smart scale (TCP). `tared` sends tare then reads net; `gross` reads gross. */
export async function readWeightFromSmartScale(kind: SmartScaleReadKind): Promise<string> {
  const plugin = getTcpPlugin()
  if (!plugin) {
    throw new Error('Smart scale is only available in the mobile app.')
  }

  const { ip, port } = readScaleSettings()
  let latest: string | null = null
  let settled = false

  return new Promise<string>((resolve, reject) => {
    const finish = (err?: Error) => {
      if (settled) return
      settled = true
      try {
        plugin.onReceive(() => {})
        plugin.onDisconnect(() => {})
        plugin.disconnect()
      } catch {
        /* ignore */
      }
      if (err) reject(err)
      else if (latest) resolve(latest)
      else reject(new Error('No weight reading received from scale.'))
    }

    const timer = setTimeout(() => finish(new Error('Smart scale read timed out.')), READ_TIMEOUT_MS)

    plugin.onReceive((res) => {
      if (res?.code !== 1) return
      const parsed = parseWeightFromMessage(String(res.data ?? ''))
      if (parsed) latest = parsed
    })

    plugin.onDisconnect(() => {
      if (!settled && latest) {
        clearTimeout(timer)
        finish()
      }
    })

    connectTcp(plugin, ip, port)
      .then(() => {
        if (kind === 'tared') {
          try {
            plugin.sendStr({ message: 'T\r\n' })
          } catch {
            /* ignore */
          }
          setTimeout(() => {
            try {
              plugin.sendStr({ message: 'W\r\n' })
            } catch {
              /* ignore */
            }
          }, 600)
        } else {
          try {
            plugin.sendStr({ message: 'W\r\n' })
          } catch {
            /* ignore */
          }
        }
      })
      .catch((e) => {
        clearTimeout(timer)
        finish(e instanceof Error ? e : new Error(String(e)))
      })

    const poll = setInterval(() => {
      if (latest && !settled) {
        clearInterval(poll)
        clearTimeout(timer)
        finish()
      }
    }, 200)
  })
}