# Tongsheng TSDZ2 mid-drive motor device spec
# Copyright 2026 Pigs Can Fly Labs LLC
# SPDX-License-Identifier: Apache-2.0

device:
  name: "Tongsheng TSDZ2 Mid-Drive"
  manufacturer: "Tongsheng"
  manufacturer_status: "active"
  protocol: "uart"
  notes: >
    Torque-sensing e-bike mid-drive conversion kit. No radio and no app: the
    controller talks to the display over plain 9600-baud TTL serial on the main
    harness, so reaching it from a phone or hub needs a bridge.

    Unlike the Bafang BBS02 this is NOT a request/response configuration
    protocol. Both ends push fixed-shape packets continuously and neither waits
    for the other -- the motor reports status 8 times a second, the display
    asserts control 15 times a second. There is no "read the settings block"
    exchange.

    CONSEQUENCE FOR IMPLEMENTERS: the display packet IS the write path. Wheel
    size and max speed are re-asserted on every repetition, so anything that
    transmits on this bus is setting them, continuously. A read-only tool here
    means listen-only -- never transmit.

    FIRMWARE MATTERS: the layouts below describe the STOCK Tongsheng firmware.
    Open-source firmware (OSF) is widely installed and changes both the feature
    set and, on 850C/860C/SW102 displays, the link itself. A capture that
    disagrees with this spec most likely means OSF is installed -- establish
    which firmware is running before concluding the spec is wrong.

    VERIFICATION: `reported` throughout -- documented by community work
    (hurzhurz/tsdz2) and corroborated by independent OSF forks, but nothing here
    has been captured from a physical unit by us. Display packet bytes 2 and 4
    are undocumented even in the sources: unknown, NOT assumed zero.

  setup:
    required: false
    confidence: "medium"
    notes: >
      Nothing to provision. No radio, no account, no pairing. Connecting at the right baud rate is the entire onboarding flow.
    methods:
      - type: "wired"
        description: >
          Connect a 3.3V USB-TTL adapter to the brown (motor TX) and orange (motor RX) harness lines at 9600 8N1. The telemetry stream is already running; nothing needs to be requested or negotiated.
        verified: false
    factory_reset:
      applicable: false
      effect: >
        No factory reset exists on the stock firmware. Settings are asserted by the display on every control packet rather than stored as provisioning state, so restoring defaults means sending default values -- or refitting the stock display. Reflashing (OSF or stock) is a firmware operation, not a reset.
    rejoin:
      requires_factory_reset: false
      notes: >
        No network binding exists, so there is nothing to rejoin. Moving the
        vehicle between owners or workshops changes nothing about how the bus
        is reached.
    credentials:
      wifi_passphrase_protection: "not_applicable"
      notes: >
        No WiFi, no account and no passphrase: physical access to the harness
        is the only credential.

bus:
  link:
    type: "uart"
    baud: 9600
    framing: "8N1"
    logic_level: "TTL"
    verification: "reported"
    wiring:
      - signal: "motor TX (display RX)"
        wire_colour: "brown"
        connector: "6-pin Tongsheng"
        verification: "reported"
      - signal: "motor RX (display TX)"
        wire_colour: "orange"
        connector: "6-pin Tongsheng"
        verification: "reported"
  style: "stream"
  checksum:
    algorithm: "sum8"
    scope: >
      8-bit sum of all preceding bytes in the packet, same rule in both
      directions (unlike the Bafang BBS02, whose rule differs by direction).
    verification: "reported"
  notes: >
    Stock displays sharing this protocol: VLCD5, VLCD6, XH18. OSF additionally
    supports SW102, DZ41, 850C and 860C.
  messages:
    - name: "motor_status"
      description: >
        Motor to display status packet. Free-running; no request triggers it.
      direction: "from_device"
      start_byte: "43"
      length: 9
      rate_hz: 8
      writes: false
      verification: "reported"
      fields:
        - offset: 0
          length: 1
          name: "start_byte"
          type: "uint8"
          encoding: "always 0x43"
          verification: "reported"
        - offset: 1
          length: 1
          name: "battery_level"
          type: "uint8"
          encoding: "0x00 red blinking; 0x01-0x09 red through green; 0x0A and above full green"
          verification: "reported"
        - offset: 2
          length: 1
          name: "status_flags"
          type: "bitfield"
          encoding: "bits: low voltage, unknown, motor running, PAS status"
          notes: "Exact bit order worth confirming by diffing single-variable changes."
          verification: "hypothesis"
        - offset: 3
          length: 1
          name: "torque_sensor_tara"
          type: "uint8"
          notes: "Zero/reference value; pair with torque_sensor_value to derive applied torque. Scaling to real units is not documented."
          verification: "reported"
        - offset: 4
          length: 1
          name: "torque_sensor_value"
          type: "uint8"
          verification: "reported"
        - offset: 5
          length: 1
          name: "error_code"
          type: "uint8"
          encoding: "0x08 = undervoltage; other codes not documented"
          verification: "reported"
        - offset: 6
          length: 2
          name: "speed"
          type: "uint16"
          endianness: "little"
          verification: "reported"
        - offset: 8
          length: 1
          name: "checksum"
          type: "uint8"
          verification: "reported"

    - name: "display_control"
      description: >
        Display to motor control packet. Re-asserted 15 times a second; this is
        the write path -- there is no separate configuration exchange.
      direction: "to_device"
      start_byte: "59"
      length: 7
      rate_hz: 15
      writes: true
      advanced: true
      advanced_reason: >
        Transmitting this packet makes you the display: it continuously asserts
        wheel size and max speed. Raising the max-speed byte is derestriction,
        which can move a pedelec out of EAPC classification and change what
        licence, insurance and road access apply. Capture the values the stock
        display sends before replacing them, so you can restore them. Do not
        transmit onto a bus that still has the stock display attached -- two
        transmitters asserting different values is untested.
      verification: "reported"
      fields:
        - offset: 0
          length: 1
          name: "start_byte"
          type: "uint8"
          encoding: "always 0x59"
          verification: "reported"
        - offset: 1
          length: 1
          name: "control_flags"
          type: "bitfield"
          encoding: "bits: headlight, assist levels 2-4, assist off, 6 km/h walk mode, assist level 1, hidden level"
          notes: "Bit positions described but not precisely pinned; confirm by diffing one change at a time."
          verification: "hypothesis"
        - offset: 2
          length: 1
          name: "unknown_byte_2"
          type: "uint8"
          notes: "Undocumented in the sources. Unknown, not known-zero -- capture before assuming."
          verification: "hypothesis"
        - offset: 3
          length: 1
          name: "wheel_size"
          type: "uint8"
          unit: "in"
          encoding: "range 6-29; default 0x1A (26 inch)"
          verification: "reported"
        - offset: 4
          length: 1
          name: "unknown_byte_4"
          type: "uint8"
          notes: "Undocumented in the sources. Unknown, not known-zero."
          verification: "hypothesis"
        - offset: 5
          length: 1
          name: "max_speed"
          type: "uint8"
          unit: "km/h"
          encoding: "minimum 0x0E (14); default 0x19 (25)"
          verification: "reported"
        - offset: 6
          length: 1
          name: "checksum"
          type: "uint8"
          verification: "reported"

entities:
  - platform: "sensor"
    name: "Speed"
    device_class: "speed"
    state_field: "motor_status.speed"
  - platform: "sensor"
    name: "Battery Level"
    device_class: "battery"
    state_field: "motor_status.battery_level"
  - platform: "sensor"
    name: "Torque Sensor"
    state_field: "motor_status.torque_sensor_value"
  - platform: "binary_sensor"
    name: "Motor Running"
    state_field: "motor_status.status_flags"
  - platform: "sensor"
    name: "Error Code"
    state_field: "motor_status.error_code"
