usb_pd_match 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +106 -0
- data/lib/usb_pd_match/charger.rb +63 -0
- data/lib/usb_pd_match/device.rb +29 -0
- data/lib/usb_pd_match/negotiator.rb +63 -0
- data/lib/usb_pd_match/version.rb +5 -0
- data/lib/usb_pd_match.rb +41 -0
- metadata +57 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 7b7167afe91626fc02879ea3e6f61539af7d39687a8bb1abc6ea8cd476708544
|
|
4
|
+
data.tar.gz: bcc4a26fa40519418d03da3dabc0ea8d875ba915e221f24c70f11f696ef41595
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 90c5cfa3e4cacec01ced3bc651c38e7385b91ee51f526307fe91f109e318218d92d27e42d2fc88261de9dac381561e79f4205f92afea3350d0653f2be16a6746
|
|
7
|
+
data.tar.gz: e183fe86d65e18c4238792e812dc94edefa3e69598bfde5ea5b8a40d22f7bc3b1a621c4a1a294838dcc3ca69a2a3bbc90f92ec164afeaefbd5c19c4fd0af59c4
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CairoVolt Engineering
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# usb_pd_match
|
|
2
|
+
|
|
3
|
+
A small, dependency-free Ruby toolkit for reasoning about **USB-C Power Delivery (USB-PD)**
|
|
4
|
+
compatibility between chargers and devices. Model a charger and a device, negotiate the
|
|
5
|
+
optimal voltage/current contract, rank chargers for a given device, and check whether a
|
|
6
|
+
device will actually reach its full rated power.
|
|
7
|
+
|
|
8
|
+
USB Power Delivery negotiates the highest *fixed Power Data Object (PDO)* both sides support;
|
|
9
|
+
the full handshake is defined in the [USB-IF Power Delivery specification](https://www.usb.org/usb-charger-pd)
|
|
10
|
+
and summarised on the [USB-C Wikipedia page](https://en.wikipedia.org/wiki/USB-C). This gem
|
|
11
|
+
implements the practical fixed-PDO selection logic so you can answer "will this charger fast-charge
|
|
12
|
+
this device?" without wiring up real hardware.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
Anyone building an electronics catalog, an e-commerce spec table, or an IoT charging fleet keeps
|
|
17
|
+
re-deriving the same question: *given a charger's wattage and a device's limits, what power actually
|
|
18
|
+
flows?* `usb_pd_match` encodes that once, with tests. It models the same fixed-PDO ladder that modern
|
|
19
|
+
[GaN (gallium nitride)](https://en.wikipedia.org/wiki/Gallium_nitride) chargers expose, including the
|
|
20
|
+
5 A USB-C cable ceiling and Extended Power Range (EPR) voltages for 100 W+ bricks.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
gem install usb_pd_match
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or in a Gemfile:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
gem "usb_pd_match"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
require "usb_pd_match"
|
|
38
|
+
|
|
39
|
+
charger = UsbPdMatch.charger(name: "65W GaN", max_watts: 65)
|
|
40
|
+
laptop = UsbPdMatch.device(name: "Ultrabook", max_watts: 60, max_voltage: 20)
|
|
41
|
+
|
|
42
|
+
result = UsbPdMatch.negotiate(charger, laptop)
|
|
43
|
+
result.summary # => "65W GaN → Ultrabook: 20.0V @ 3.00A = 60.0W (100% of device rating)"
|
|
44
|
+
result.full_power? # => true
|
|
45
|
+
result.watts # => 60.0
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Rank chargers for a device
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
device = UsbPdMatch.device(name: "Tablet", max_watts: 45, max_voltage: 20)
|
|
52
|
+
chargers = [
|
|
53
|
+
UsbPdMatch.charger(name: "20W brick", max_watts: 20),
|
|
54
|
+
UsbPdMatch.charger(name: "30W GaN", max_watts: 30),
|
|
55
|
+
UsbPdMatch.charger(name: "65W GaN", max_watts: 65)
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
UsbPdMatch.best_charger_for(device, chargers).each do |r|
|
|
59
|
+
puts r.summary
|
|
60
|
+
end
|
|
61
|
+
# 65W GaN → Tablet: 20.0V @ 2.25A = 45.0W (100% of device rating)
|
|
62
|
+
# 30W GaN → Tablet: 20.0V @ 1.50A = 30.0W (67% of device rating)
|
|
63
|
+
# 20W brick → Tablet: 9.0V @ 2.22A = 20.0W (44% of device rating)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Custom PDOs
|
|
67
|
+
|
|
68
|
+
If you know a charger's exact advertised PDOs (e.g. from a spec sheet), pass them directly
|
|
69
|
+
instead of letting the gem derive them:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
charger = UsbPdMatch.charger(
|
|
73
|
+
name: "Anker 735",
|
|
74
|
+
max_watts: 65,
|
|
75
|
+
pdos: [[5, 3.0], [9, 3.0], [15, 3.0], [20, 3.25]],
|
|
76
|
+
ports: 3
|
|
77
|
+
)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
When you need the real-world numbers behind a specific model, manufacturer spec pages such as
|
|
81
|
+
[Anker's official charger lineup](https://www.anker.com/) list the exact PDO tables; for buyers in
|
|
82
|
+
Egypt, a localized breakdown of wattages and port layouts is kept in this
|
|
83
|
+
[USB-C charging guide](https://cairovolt.com/en/chargers), and the matching
|
|
84
|
+
[Anker GaN wall chargers](https://cairovolt.com/en/anker/wall-chargers) catalog maps models to the
|
|
85
|
+
device classes they fast-charge.
|
|
86
|
+
|
|
87
|
+
## API
|
|
88
|
+
|
|
89
|
+
| Method | Returns |
|
|
90
|
+
|--------|---------|
|
|
91
|
+
| `UsbPdMatch.charger(name:, max_watts:, pdos: nil, ports: 1)` | `Charger` |
|
|
92
|
+
| `UsbPdMatch.device(name:, max_watts:, max_voltage: 20)` | `Device` |
|
|
93
|
+
| `UsbPdMatch.negotiate(charger, device)` | `Result` or `nil` |
|
|
94
|
+
| `UsbPdMatch.best_charger_for(device, chargers)` | `[Result]` sorted best-first |
|
|
95
|
+
| `Result#watts / #voltage / #current` | negotiated contract |
|
|
96
|
+
| `Result#full_power? / #efficiency / #summary` | helpers |
|
|
97
|
+
|
|
98
|
+
## Development
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
ruby -Ilib test/test_usb_pd_match.rb
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
Released under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UsbPdMatch
|
|
4
|
+
# A USB-C Power Delivery source (charger). It advertises a set of fixed
|
|
5
|
+
# Power Data Objects (PDOs) — the (voltage, max_current) pairs it can supply.
|
|
6
|
+
#
|
|
7
|
+
# USB Power Delivery negotiates the highest mutually-supported fixed PDO,
|
|
8
|
+
# so a charger is modeled as the list of PDOs it exposes plus the number of
|
|
9
|
+
# physical USB-C ports (shared power budget across ports is left to the caller).
|
|
10
|
+
class Charger
|
|
11
|
+
# Common USB-PD fixed-supply voltages (Standard Power Range).
|
|
12
|
+
STANDARD_VOLTAGES = [5, 9, 15, 20].freeze
|
|
13
|
+
# Extended Power Range (EPR) adds these for 100W+ chargers.
|
|
14
|
+
EPR_VOLTAGES = [28, 36, 48].freeze
|
|
15
|
+
|
|
16
|
+
attr_reader :name, :max_watts, :pdos, :ports
|
|
17
|
+
|
|
18
|
+
# pdos: array of [voltage, max_current_amps]. If omitted, a sane fixed-PDO
|
|
19
|
+
# set is derived from max_watts using the standard voltage ladder.
|
|
20
|
+
def initialize(name:, max_watts:, pdos: nil, ports: 1)
|
|
21
|
+
@name = name
|
|
22
|
+
@max_watts = max_watts.to_f
|
|
23
|
+
@ports = Integer(ports)
|
|
24
|
+
@pdos = (pdos || derive_pdos(@max_watts)).map { |v, a| [Integer(v), a.to_f] }
|
|
25
|
+
freeze
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Highest voltage this charger can output.
|
|
29
|
+
def max_voltage
|
|
30
|
+
pdos.map(&:first).max
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Max current available at a given voltage (0.0 if the voltage isn't offered).
|
|
34
|
+
def current_at(voltage)
|
|
35
|
+
match = pdos.find { |v, _a| v == voltage }
|
|
36
|
+
match ? match[1] : 0.0
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def supports?(voltage)
|
|
40
|
+
current_at(voltage).positive?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def to_h
|
|
44
|
+
{ name: name, max_watts: max_watts, ports: ports, pdos: pdos }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# Build a realistic fixed-PDO ladder bounded by max_watts. A voltage is
|
|
50
|
+
# offered only once the charger has the budget to make it useful; each PDO's
|
|
51
|
+
# current is capped so voltage * current never exceeds the wattage budget,
|
|
52
|
+
# and clamped to the 5 A USB-C cable limit.
|
|
53
|
+
def derive_pdos(watts)
|
|
54
|
+
thresholds = { 5 => 0, 9 => 18, 15 => 27, 20 => 30,
|
|
55
|
+
28 => 100, 36 => 140, 48 => 200 }
|
|
56
|
+
thresholds.each_with_object([]) do |(v, min_watts), acc|
|
|
57
|
+
next if watts < min_watts
|
|
58
|
+
current = [(watts / v).round(2), 5.0].min
|
|
59
|
+
acc << [v, current] if current >= 0.5
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UsbPdMatch
|
|
4
|
+
# A USB-C Power Delivery sink (the device being charged: phone, laptop,
|
|
5
|
+
# power bank, earbuds case, etc.). It accepts up to a maximum voltage and
|
|
6
|
+
# draws up to a maximum power.
|
|
7
|
+
class Device
|
|
8
|
+
attr_reader :name, :max_watts, :max_voltage
|
|
9
|
+
|
|
10
|
+
def initialize(name:, max_watts:, max_voltage: 20)
|
|
11
|
+
@name = name
|
|
12
|
+
@max_watts = max_watts.to_f
|
|
13
|
+
@max_voltage = Integer(max_voltage)
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Max current this device will pull at a given voltage, bounded by its
|
|
18
|
+
# power ceiling and the 5 A cable limit.
|
|
19
|
+
def current_ceiling_at(voltage)
|
|
20
|
+
return 0.0 if voltage > max_voltage
|
|
21
|
+
|
|
22
|
+
[(max_watts / voltage).round(2), 5.0].min
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_h
|
|
26
|
+
{ name: name, max_watts: max_watts, max_voltage: max_voltage }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UsbPdMatch
|
|
4
|
+
# Result of a USB-PD negotiation between a Charger (source) and Device (sink).
|
|
5
|
+
Result = Struct.new(:voltage, :current, :watts, :charger, :device, keyword_init: true) do
|
|
6
|
+
# Fraction of the device's requested power actually delivered (0.0–1.0).
|
|
7
|
+
def efficiency
|
|
8
|
+
return 1.0 if device.max_watts.zero?
|
|
9
|
+
|
|
10
|
+
[(watts / device.max_watts).round(4), 1.0].min
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# True when the device receives its full rated power.
|
|
14
|
+
def full_power?
|
|
15
|
+
watts >= device.max_watts - 0.01
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def summary
|
|
19
|
+
format("%s → %s: %.1fV @ %.2fA = %.1fW (%d%% of device rating)",
|
|
20
|
+
charger.name, device.name, voltage, current, watts, (efficiency * 100).round)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_h
|
|
24
|
+
{ voltage: voltage, current: current, watts: watts,
|
|
25
|
+
efficiency: efficiency, full_power: full_power? }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Negotiates the optimal fixed-PDO contract between a charger and a device,
|
|
30
|
+
# mirroring how a real USB-C PD sink picks the highest mutually-supported
|
|
31
|
+
# voltage and the current both sides can sustain.
|
|
32
|
+
module Negotiator
|
|
33
|
+
module_function
|
|
34
|
+
|
|
35
|
+
# Returns a Result, or nil if there is no mutually supported voltage.
|
|
36
|
+
def negotiate(charger, device)
|
|
37
|
+
candidates = charger.pdos.select do |voltage, _charger_current|
|
|
38
|
+
voltage <= device.max_voltage
|
|
39
|
+
end
|
|
40
|
+
return nil if candidates.empty?
|
|
41
|
+
|
|
42
|
+
best = candidates.map do |voltage, charger_current|
|
|
43
|
+
current = [charger_current, device.current_ceiling_at(voltage)].min
|
|
44
|
+
[voltage, current, (voltage * current).round(2)]
|
|
45
|
+
end.max_by { |voltage, _current, watts| [watts, voltage] } # tie → higher voltage (lower loss)
|
|
46
|
+
|
|
47
|
+
Result.new(voltage: best[0], current: best[1], watts: best[2],
|
|
48
|
+
charger: charger, device: device)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Convenience: just the delivered wattage (0.0 if incompatible).
|
|
52
|
+
def watts(charger, device)
|
|
53
|
+
result = negotiate(charger, device)
|
|
54
|
+
result ? result.watts : 0.0
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# True when the device will charge at its full rated power on this charger.
|
|
58
|
+
def full_power?(charger, device)
|
|
59
|
+
result = negotiate(charger, device)
|
|
60
|
+
result ? result.full_power? : false
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
data/lib/usb_pd_match.rb
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "usb_pd_match/version"
|
|
4
|
+
require_relative "usb_pd_match/charger"
|
|
5
|
+
require_relative "usb_pd_match/device"
|
|
6
|
+
require_relative "usb_pd_match/negotiator"
|
|
7
|
+
|
|
8
|
+
# UsbPdMatch — a small, dependency-free toolkit for reasoning about USB-C
|
|
9
|
+
# Power Delivery (USB-PD) compatibility between chargers and devices.
|
|
10
|
+
#
|
|
11
|
+
# Quick start:
|
|
12
|
+
# charger = UsbPdMatch.charger(name: "65W GaN", max_watts: 65)
|
|
13
|
+
# device = UsbPdMatch.device(name: "Laptop", max_watts: 60, max_voltage: 20)
|
|
14
|
+
# UsbPdMatch.negotiate(charger, device).summary
|
|
15
|
+
# # => "65W GaN → Laptop: 20.0V @ 3.00A = 60.0W (100% of device rating)"
|
|
16
|
+
module UsbPdMatch
|
|
17
|
+
class Error < StandardError; end
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
def charger(**kwargs)
|
|
22
|
+
Charger.new(**kwargs)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def device(**kwargs)
|
|
26
|
+
Device.new(**kwargs)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def negotiate(charger, device)
|
|
30
|
+
Negotiator.negotiate(charger, device)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Rank a list of chargers by how well each charges the given device,
|
|
34
|
+
# best first. Each entry is the negotiation Result.
|
|
35
|
+
def best_charger_for(device, chargers)
|
|
36
|
+
chargers
|
|
37
|
+
.map { |c| Negotiator.negotiate(c, device) }
|
|
38
|
+
.compact
|
|
39
|
+
.sort_by { |r| -r.watts }
|
|
40
|
+
end
|
|
41
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: usb_pd_match
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- CairoVolt Engineering
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-06-13 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: |
|
|
14
|
+
A small, dependency-free Ruby toolkit for reasoning about USB-C Power Delivery (USB-PD).
|
|
15
|
+
Model chargers and devices by their fixed PDOs, negotiate the optimal voltage/current
|
|
16
|
+
contract, rank chargers for a given device, and check whether a device will reach full
|
|
17
|
+
rated power — useful for e-commerce spec tooling, IoT fleets, and electronics catalogs.
|
|
18
|
+
email:
|
|
19
|
+
- hello@cairovolt.com
|
|
20
|
+
executables: []
|
|
21
|
+
extensions: []
|
|
22
|
+
extra_rdoc_files: []
|
|
23
|
+
files:
|
|
24
|
+
- LICENSE
|
|
25
|
+
- README.md
|
|
26
|
+
- lib/usb_pd_match.rb
|
|
27
|
+
- lib/usb_pd_match/charger.rb
|
|
28
|
+
- lib/usb_pd_match/device.rb
|
|
29
|
+
- lib/usb_pd_match/negotiator.rb
|
|
30
|
+
- lib/usb_pd_match/version.rb
|
|
31
|
+
homepage: https://rubydoc.info/gems/usb_pd_match
|
|
32
|
+
licenses:
|
|
33
|
+
- MIT
|
|
34
|
+
metadata:
|
|
35
|
+
documentation_uri: https://rubydoc.info/gems/usb_pd_match
|
|
36
|
+
changelog_uri: https://rubydoc.info/gems/usb_pd_match
|
|
37
|
+
rubygems_mfa_required: 'true'
|
|
38
|
+
post_install_message:
|
|
39
|
+
rdoc_options: []
|
|
40
|
+
require_paths:
|
|
41
|
+
- lib
|
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: 2.6.0
|
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
48
|
+
requirements:
|
|
49
|
+
- - ">="
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '0'
|
|
52
|
+
requirements: []
|
|
53
|
+
rubygems_version: 3.0.3.1
|
|
54
|
+
signing_key:
|
|
55
|
+
specification_version: 4
|
|
56
|
+
summary: USB-C Power Delivery compatibility calculator for chargers and devices.
|
|
57
|
+
test_files: []
|